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,172 @@
1
+ import fsp from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { exists, nowIso, readJson, writeJsonAtomic } from './fsx.js';
4
+ import { ARCHITECTURE_CAPSULE_ARTIFACT, ARCHITECTURE_MAP_BASELINE_ARTIFACT, ARCHITECTURE_MAP_MANIFEST_ARTIFACT, ARCHITECTURE_MAP_REVIEW_ARTIFACT, bindArchitectureMapBaseline, buildAfterReview, sealBaseline, validateArchitectureMapBaselineArtifact, validateArchitectureMapReviewArtifact } from './architecture-map-review.js';
5
+ import { readContextGraphSnapshot } from './triwiki/context-graph/store/snapshot-store.js';
6
+ export { ARCHITECTURE_MAP_REVIEW_ARTIFACT };
7
+ export const ARCHITECTURE_MAP_BASELINE_STAGE = 'architecture_map_baseline';
8
+ export const ARCHITECTURE_MAP_REVIEW_STAGE = 'architecture_map_review';
9
+ const ARCHITECTURE_MAP_EXEMPT_ROUTES = new Set([
10
+ 'Answer',
11
+ 'DFix',
12
+ 'Help',
13
+ 'Wiki',
14
+ 'Goal',
15
+ 'GX',
16
+ 'DB'
17
+ ]);
18
+ export function routeNeedsArchitectureMap(route) {
19
+ const id = String(route?.id || '');
20
+ if (!id)
21
+ return false;
22
+ if (ARCHITECTURE_MAP_EXEMPT_ROUTES.has(id))
23
+ return false;
24
+ return true;
25
+ }
26
+ export function createArchitectureMapPlanBinding() {
27
+ return Object.freeze({
28
+ baseline_artifact: ARCHITECTURE_MAP_BASELINE_ARTIFACT,
29
+ review_artifact: ARCHITECTURE_MAP_REVIEW_ARTIFACT,
30
+ capsule_artifact: ARCHITECTURE_CAPSULE_ARTIFACT,
31
+ manifest_artifact: ARCHITECTURE_MAP_MANIFEST_ARTIFACT,
32
+ required: true,
33
+ seeded_at: nowIso()
34
+ });
35
+ }
36
+ export function planStagesArchitectureMap(plan) {
37
+ return Array.isArray(plan?.stages)
38
+ && plan.stages.some((stage) => String(stage?.id || '') === ARCHITECTURE_MAP_BASELINE_STAGE
39
+ && !['skipped', 'not_applicable'].includes(String(stage?.status || '')));
40
+ }
41
+ export async function seedArchitectureMapPlanBinding(dir, plan) {
42
+ if (!planStagesArchitectureMap(plan))
43
+ return plan;
44
+ plan.architecture_map = createArchitectureMapPlanBinding();
45
+ await writeJsonAtomic(path.join(dir, 'pipeline-plan.json'), plan);
46
+ return plan;
47
+ }
48
+ export async function maybeSeedArchitectureMapForPlan(input) {
49
+ if (!input.plan?.architecture_map || !input.missionId)
50
+ return;
51
+ await seedArchitectureMapBaselineArtifacts({
52
+ root: input.root,
53
+ dir: input.dir,
54
+ missionId: String(input.missionId),
55
+ routeId: String(input.plan.route?.id || input.plan.route?.command || 'SKS'),
56
+ taskProfile: String(input.taskProfile || input.plan.task_profile || '')
57
+ });
58
+ }
59
+ export async function seedArchitectureMapBaselineArtifacts(input) {
60
+ const baselinePath = path.join(input.dir, ARCHITECTURE_MAP_BASELINE_ARTIFACT);
61
+ if (await exists(baselinePath))
62
+ return { ok: true, baselinePath };
63
+ const load = await readContextGraphSnapshot(input.root);
64
+ if (load.status !== 'ok' || !load.snapshot) {
65
+ return { ok: false, reason: load.blocker || 'architecture_map_graph_unavailable' };
66
+ }
67
+ const sealed = sealBaseline({
68
+ missionId: input.missionId,
69
+ routeId: input.routeId,
70
+ snapshot: load.snapshot,
71
+ policyRoot: input.root,
72
+ capturedAt: nowIso(),
73
+ ...(input.taskProfile ? { taskProfile: input.taskProfile } : {})
74
+ });
75
+ await fsp.mkdir(input.dir, { recursive: true });
76
+ await writeJsonAtomic(baselinePath, sealed.baseline);
77
+ await writeJsonAtomic(path.join(input.dir, ARCHITECTURE_MAP_MANIFEST_ARTIFACT), sealed.manifest);
78
+ await writeArchitectureMapCapsule(input.dir, sealed.capsule.text);
79
+ return { ok: true, baselinePath };
80
+ }
81
+ export async function ensureArchitectureMapReviewArtifacts(input) {
82
+ const reviewPath = path.join(input.dir, ARCHITECTURE_MAP_REVIEW_ARTIFACT);
83
+ if (await exists(reviewPath))
84
+ return { ok: true, reviewPath };
85
+ const baselinePath = path.join(input.dir, ARCHITECTURE_MAP_BASELINE_ARTIFACT);
86
+ if (!(await exists(baselinePath))) {
87
+ return { ok: false, reason: 'architecture_map_baseline_missing' };
88
+ }
89
+ const baseline = await readJson(baselinePath, null);
90
+ if (!baseline)
91
+ return { ok: false, reason: 'architecture_map_baseline_unreadable' };
92
+ const load = await readContextGraphSnapshot(input.root);
93
+ if (load.status !== 'ok' || !load.snapshot) {
94
+ return { ok: false, reason: load.blocker || 'architecture_map_graph_unavailable' };
95
+ }
96
+ const built = buildAfterReview({
97
+ missionId: input.missionId,
98
+ ...(input.routeId ? { routeId: input.routeId } : {}),
99
+ baseline,
100
+ afterSnapshot: load.snapshot,
101
+ policyRoot: input.root,
102
+ generatedAt: nowIso()
103
+ });
104
+ await fsp.mkdir(input.dir, { recursive: true });
105
+ await writeJsonAtomic(reviewPath, built.review);
106
+ await writeJsonAtomic(path.join(input.dir, ARCHITECTURE_MAP_MANIFEST_ARTIFACT), built.manifest);
107
+ await writeArchitectureMapCapsule(input.dir, built.capsule.text);
108
+ await writeJsonAtomic(path.join(input.dir, 'architecture-map-delta.json'), built.delta);
109
+ return { ok: true, reviewPath };
110
+ }
111
+ export async function architectureMapGateStatus(root, state = {}) {
112
+ if (state?.architecture_map_required !== true)
113
+ return { ok: true, blockers: [] };
114
+ const missionId = state?.mission_id;
115
+ if (!missionId)
116
+ return { ok: false, blockers: ['architecture_map_mission_missing'] };
117
+ const dir = path.join(root, '.sneakoscope/missions', missionId);
118
+ const plan = await readJson(path.join(dir, 'pipeline-plan.json'), null);
119
+ if (!plan?.architecture_map)
120
+ return { ok: true, blockers: [], not_applicable: true };
121
+ if (await exists(path.join(dir, ARCHITECTURE_MAP_BASELINE_ARTIFACT))) {
122
+ await ensureArchitectureMapReviewArtifacts({
123
+ root,
124
+ dir,
125
+ missionId,
126
+ routeId: String(plan?.route?.id || state?.route || '')
127
+ });
128
+ }
129
+ const blockers = [];
130
+ const baselinePath = path.join(dir, ARCHITECTURE_MAP_BASELINE_ARTIFACT);
131
+ const reviewPath = path.join(dir, ARCHITECTURE_MAP_REVIEW_ARTIFACT);
132
+ if (!(await exists(baselinePath))) {
133
+ blockers.push('architecture_map_baseline_missing');
134
+ }
135
+ else {
136
+ const baseline = await readJson(baselinePath, null);
137
+ const validation = validateArchitectureMapBaselineArtifact(baseline);
138
+ if (!validation.ok)
139
+ blockers.push('architecture_map_baseline_invalid', ...validation.blockers.map((b) => `baseline:${b}`));
140
+ else {
141
+ const binding = bindArchitectureMapBaseline({ baseline: baseline, missionId });
142
+ if (!binding.ok)
143
+ blockers.push(...binding.blockers.map((b) => `architecture_map_baseline_${b}`));
144
+ if (baseline && baseline.capturedBeforeMutation !== true)
145
+ blockers.push('architecture_map_baseline_late');
146
+ }
147
+ }
148
+ if (!(await exists(reviewPath))) {
149
+ blockers.push('architecture_map_review_missing');
150
+ }
151
+ else {
152
+ const review = await readJson(reviewPath, null);
153
+ const validation = validateArchitectureMapReviewArtifact(review);
154
+ if (!validation.ok)
155
+ blockers.push('architecture_map_review_invalid', ...validation.blockers.map((b) => `review:${b}`));
156
+ else if (review?.verdict !== 'pass') {
157
+ blockers.push('architecture_map_review_blocked');
158
+ if (Array.isArray(review?.unaccountedChangedPaths) && review.unaccountedChangedPaths.length) {
159
+ blockers.push('architecture_map_unaccounted_changed_file');
160
+ }
161
+ for (const id of review?.blockingFindingIds || [])
162
+ blockers.push(`architecture_map_finding:${id}`);
163
+ }
164
+ }
165
+ return { ok: blockers.length === 0, blockers: [...new Set(blockers)] };
166
+ }
167
+ export async function writeArchitectureMapCapsule(dir, text) {
168
+ const file = path.join(dir, ARCHITECTURE_CAPSULE_ARTIFACT);
169
+ await fsp.mkdir(dir, { recursive: true });
170
+ await fsp.writeFile(file, text.endsWith('\n') ? text : `${text}\n`, 'utf8');
171
+ return file;
172
+ }
@@ -0,0 +1,180 @@
1
+ import { sha256 } from './fsx.js';
2
+ import { ARCHITECTURE_BASELINE_SCHEMA, ARCHITECTURE_MAP_MANIFEST_SCHEMA, ARCHITECTURE_REVIEW_SCHEMA } from './triwiki/context-graph/architecture/contracts.js';
3
+ import { buildArchitectureBaseline, verifyArchitectureBaselineSeal } from './triwiki/context-graph/architecture/baseline.js';
4
+ import { renderArchitectureCapsule } from './triwiki/context-graph/architecture/capsule.js';
5
+ import { buildArchitectureDelta, buildArchitectureReview } from './triwiki/context-graph/architecture/delta.js';
6
+ import { buildArchitectureInputBundle, policyContentHash } from './triwiki/context-graph/architecture/input-bundle.js';
7
+ import { loadArchitectureMapPolicy } from './triwiki/context-graph/architecture/policy.js';
8
+ import { buildArchitectureScope } from './triwiki/context-graph/architecture/slice.js';
9
+ import { validateArchitectureBaseline, validateArchitectureInputBundle, validateArchitectureMapManifest, validateArchitectureReview } from './triwiki/context-graph/architecture/validation.js';
10
+ import { buildArchitectureMapManifest, buildArchitectureMapViews } from './triwiki/context-graph/projections/mermaid/index.js';
11
+ export const ARCHITECTURE_MAP_BASELINE_ARTIFACT = 'architecture-map-baseline.json';
12
+ export const ARCHITECTURE_MAP_REVIEW_ARTIFACT = 'architecture-map-review.json';
13
+ export const ARCHITECTURE_CAPSULE_ARTIFACT = 'architecture-capsule.txt';
14
+ export const ARCHITECTURE_MAP_MANIFEST_ARTIFACT = 'architecture-map-manifest.json';
15
+ export const ARCHITECTURE_MAP_REVIEW_SCHEMA = ARCHITECTURE_REVIEW_SCHEMA;
16
+ export const ARCHITECTURE_MAP_BASELINE_SCHEMA = ARCHITECTURE_BASELINE_SCHEMA;
17
+ export const ARCHITECTURE_MAP_MANIFEST_SCHEMA_ID = ARCHITECTURE_MAP_MANIFEST_SCHEMA;
18
+ export function resolveArchitectureMapPolicy(input) {
19
+ if (input.policy)
20
+ return input.policy;
21
+ if (input.policyRoot)
22
+ return loadArchitectureMapPolicy(input.policyRoot);
23
+ throw new Error('architecture_map_policy_required');
24
+ }
25
+ export function sealBaseline(input) {
26
+ const policy = resolveArchitectureMapPolicy(input);
27
+ const rootId = input.rootId ?? 'workspace';
28
+ const profile = input.profile ?? 'global';
29
+ const bundle = buildArchitectureInputBundle({
30
+ rootId,
31
+ graph: input.snapshot,
32
+ policy,
33
+ profile
34
+ });
35
+ const views = buildArchitectureMapViews(input.snapshot, policy, {
36
+ rootId,
37
+ missionId: input.missionId
38
+ });
39
+ const viewHashes = Object.freeze(Object.fromEntries(views.views.map((view) => [view.viewId, view.projection.contentHash])));
40
+ const scope = buildArchitectureScope({
41
+ profile,
42
+ ...(input.seedNodeIds ? { seedNodeIds: input.seedNodeIds } : {}),
43
+ ...(input.seedPaths ? { seedPaths: input.seedPaths } : {})
44
+ });
45
+ const baseline = buildArchitectureBaseline({
46
+ missionId: input.missionId,
47
+ routeId: input.routeId,
48
+ taskProfile: input.taskProfile ?? profile,
49
+ capturedAt: input.capturedAt,
50
+ bundle,
51
+ policy,
52
+ scope,
53
+ viewHashes,
54
+ findings: views.findings,
55
+ metrics: views.metrics
56
+ });
57
+ const capsule = renderArchitectureCapsule({
58
+ profile,
59
+ missionId: input.missionId,
60
+ routeId: input.routeId,
61
+ baseline,
62
+ metrics: views.metrics,
63
+ findings: views.findings,
64
+ ...(input.seedPaths ? { seedPaths: input.seedPaths } : {}),
65
+ mapArtifact: ARCHITECTURE_MAP_MANIFEST_ARTIFACT
66
+ });
67
+ return Object.freeze({
68
+ baseline,
69
+ bundle,
70
+ manifest: views.manifest,
71
+ capsule,
72
+ policy
73
+ });
74
+ }
75
+ export function buildAfterReview(input) {
76
+ const policy = resolveArchitectureMapPolicy(input);
77
+ const rootId = input.rootId ?? 'workspace';
78
+ const profile = input.profile ?? input.baseline.scope.profile;
79
+ const afterBundle = buildArchitectureInputBundle({
80
+ rootId,
81
+ graph: input.afterSnapshot,
82
+ policy,
83
+ profile,
84
+ policyHash: policyContentHash(policy)
85
+ });
86
+ const views = buildArchitectureMapViews(input.afterSnapshot, policy, {
87
+ rootId,
88
+ missionId: input.missionId
89
+ });
90
+ const projectionAccounting = Object.freeze(Object.fromEntries(views.views.map((view) => [
91
+ view.viewId,
92
+ {
93
+ selectedNodeCount: view.projection.accounting.selectedNodeIds.length,
94
+ emittedNodeCount: view.projection.accounting.emittedNodeIds.length,
95
+ omittedNodeCount: view.projection.accounting.omittedNodes.length,
96
+ selectedEdgeCount: view.projection.accounting.selectedEdgeIds.length,
97
+ emittedEdgeCount: view.projection.accounting.emittedEdgeIds.length,
98
+ omittedEdgeCount: view.projection.accounting.omittedEdges.length,
99
+ accountingRatio: view.projection.accounting.selectedNodeIds.length === 0
100
+ ? 1
101
+ : view.projection.accounting.emittedNodeIds.length /
102
+ view.projection.accounting.selectedNodeIds.length
103
+ }
104
+ ])));
105
+ const review = buildArchitectureReview({
106
+ missionId: input.missionId,
107
+ baseline: input.baseline,
108
+ afterBundle,
109
+ policy,
110
+ generatedAt: input.generatedAt,
111
+ projectionAccounting,
112
+ ...(input.accountedChangedPaths
113
+ ? { accountedChangedPaths: input.accountedChangedPaths }
114
+ : {})
115
+ });
116
+ const delta = buildArchitectureDelta({
117
+ missionId: input.missionId,
118
+ baseline: input.baseline,
119
+ afterBundle,
120
+ policy
121
+ });
122
+ const capsule = renderArchitectureCapsule({
123
+ profile,
124
+ missionId: input.missionId,
125
+ ...(input.routeId ? { routeId: input.routeId } : {}),
126
+ baseline: input.baseline,
127
+ metrics: views.metrics,
128
+ findings: views.findings,
129
+ mapArtifact: 'architecture-map-delta.mmd'
130
+ });
131
+ return Object.freeze({
132
+ review,
133
+ delta,
134
+ afterBundle,
135
+ manifest: views.manifest,
136
+ capsule,
137
+ policy
138
+ });
139
+ }
140
+ export function validateArchitectureMapBaselineArtifact(value) {
141
+ return validateArchitectureBaseline(value);
142
+ }
143
+ export function validateArchitectureMapReviewArtifact(value) {
144
+ return validateArchitectureReview(value);
145
+ }
146
+ export function validateArchitectureMapManifestArtifact(value) {
147
+ return validateArchitectureMapManifest(value);
148
+ }
149
+ export function validateArchitectureMapInputBundleArtifact(value) {
150
+ return validateArchitectureInputBundle(value);
151
+ }
152
+ export function bindArchitectureMapBaseline(input) {
153
+ const validation = validateArchitectureBaseline(input.baseline);
154
+ const blockers = [...validation.blockers];
155
+ if (input.baseline.missionId !== input.missionId)
156
+ blockers.push('mission_mismatch');
157
+ if (!verifyArchitectureBaselineSeal(input.baseline))
158
+ blockers.push('seal_mismatch');
159
+ if (blockers.length)
160
+ return { ok: false, blockers, binding: null };
161
+ return {
162
+ ok: true,
163
+ blockers: [],
164
+ binding: {
165
+ missionId: input.baseline.missionId,
166
+ seal: input.baseline.seal,
167
+ canonicalPayloadHash: input.baseline.canonicalPayloadHash,
168
+ policyHash: input.baseline.policyHash,
169
+ graphHash: input.baseline.graphHash,
170
+ worktreeFingerprintHash: input.baseline.worktreeFingerprintHash
171
+ }
172
+ };
173
+ }
174
+ export function architectureMapReviewDigest(review) {
175
+ return sha256(JSON.stringify(review ?? null));
176
+ }
177
+ export function architectureMapBaselineDigest(baseline) {
178
+ return sha256(JSON.stringify(baseline ?? null));
179
+ }
180
+ export { buildArchitectureMapManifest, buildArchitectureMapViews };
@@ -17,7 +17,8 @@ export async function repairCodexAppFastUi(root = process.cwd(), input = {}) {
17
17
  const candidates = [
18
18
  { scope: 'project', file: path.join(resolvedRoot, '.codex', 'config.toml'), mode: 'project_forbidden_keys' },
19
19
  { scope: 'codex_home', file: path.join(home, 'config.toml'), mode: 'sks_caused_host_owned_keys' }
20
- ];
20
+ ].filter((candidate) => candidate.mode !== 'project_forbidden_keys'
21
+ || path.resolve(candidate.file) !== path.resolve(home, 'config.toml'));
21
22
  const actions = [];
22
23
  const detectedProjectLocalForbiddenKeys = [];
23
24
  const unsafeReasons = [];
@@ -24,6 +24,7 @@ export const NATIVE_SOURCE_FILES = [
24
24
  'ProvidersReliability.swift',
25
25
  'ProvidersRoutingTruth.swift',
26
26
  'ProvidersOpenRouter.swift',
27
+ 'ProvidersModelExposure.swift',
27
28
  'ProvidersBridgeCatalog.swift',
28
29
  'RemoteCodingViewController.swift',
29
30
  'DiagnosticsViewController.swift',
@@ -135,6 +135,7 @@ export async function readCodexLbModelCatalog(opts = {}) {
135
135
  ok: false,
136
136
  status: 'blocked',
137
137
  models: [],
138
+ model_rows: [],
138
139
  model_efforts: {},
139
140
  http_status: null,
140
141
  blockers: loaded.missing.length ? loaded.missing.map((item) => `codex_lb_missing:${item}`) : ['codex_lb_not_configured']
@@ -147,6 +148,7 @@ export async function readCodexLbModelCatalog(opts = {}) {
147
148
  ok: false,
148
149
  status: 'blocked',
149
150
  models: [],
151
+ model_rows: [],
150
152
  model_efforts: {},
151
153
  http_status: null,
152
154
  blockers: [transportBlocker]
@@ -167,6 +169,7 @@ export async function readCodexLbModelCatalog(opts = {}) {
167
169
  ok: false,
168
170
  status: 'blocked',
169
171
  models: [],
172
+ model_rows: [],
170
173
  model_efforts: {},
171
174
  http_status: null,
172
175
  blockers: [`codex_lb_${safeBridgeErrorCode(error).replace(/^bridge_/, '')}`]
@@ -175,15 +178,17 @@ export async function readCodexLbModelCatalog(opts = {}) {
175
178
  try {
176
179
  const timeoutMs = Math.max(250, Number(opts.timeoutMs || 5000));
177
180
  const modelsUrl = new URL(`${loaded.base_url}/models`);
178
- const response = opts.fetchImpl
179
- ? await fetchImpl(modelsUrl, {
181
+ const request = async (url) => (opts.fetchImpl
182
+ ? await fetchImpl(url, {
180
183
  headers: gatewayHeaders,
181
184
  redirect: 'error',
182
185
  signal: AbortSignal.timeout(timeoutMs)
183
186
  })
184
- : await requestPinnedCodexLbCatalog(modelsUrl, remote, gatewayHeaders, timeoutMs);
187
+ : await requestPinnedCodexLbCatalog(url, remote, gatewayHeaders, timeoutMs));
188
+ const response = await request(modelsUrl);
185
189
  const payload = await response.json().catch(() => null);
186
190
  const models = normalizeCodexLbModelCatalogPayload(payload);
191
+ const modelRows = codexLbModelCatalogRows(payload);
187
192
  const apiEfforts = normalizeCodexModelEffortCatalogPayload(payload);
188
193
  const cachePath = path.join(String(process.env.CODEX_HOME || path.join(os.homedir(), '.codex')), 'models_cache.json');
189
194
  const cachePayload = await readJson(cachePath, null).catch(() => null);
@@ -195,6 +200,7 @@ export async function readCodexLbModelCatalog(opts = {}) {
195
200
  ok,
196
201
  status: ok ? 'ready' : 'blocked',
197
202
  models,
203
+ model_rows: modelRows,
198
204
  model_efforts: modelEfforts,
199
205
  http_status: response.status,
200
206
  blockers: [
@@ -209,6 +215,7 @@ export async function readCodexLbModelCatalog(opts = {}) {
209
215
  ok: false,
210
216
  status: 'blocked',
211
217
  models: [],
218
+ model_rows: [],
212
219
  model_efforts: {},
213
220
  http_status: null,
214
221
  blockers: ['codex_lb_model_catalog_unavailable']
@@ -287,6 +294,21 @@ export function normalizeCodexLbModelCatalogPayload(payload) {
287
294
  .filter(Boolean);
288
295
  return [...new Set(models)];
289
296
  }
297
+ export function codexLbModelCatalogRows(payload) {
298
+ const rows = Array.isArray(payload?.models) ? payload.models : Array.isArray(payload?.data) ? payload.data : [];
299
+ const seen = new Set();
300
+ const preserved = [];
301
+ for (const row of rows) {
302
+ if (!row || typeof row !== 'object' || Array.isArray(row))
303
+ continue;
304
+ const slug = String(row.id || row.model || row.slug || row.name || '').trim();
305
+ if (!slug || seen.has(slug))
306
+ continue;
307
+ seen.add(slug);
308
+ preserved.push({ ...row, slug });
309
+ }
310
+ return preserved;
311
+ }
290
312
  export function normalizeCodexLbBaseUrl(input = '') {
291
313
  let host = String(input || '').trim();
292
314
  if (!host)
@@ -2,14 +2,18 @@ import { buildBridgeRouteIndex, sha256Stable } from '../route-index.js';
2
2
  import { COMBINED_BRIDGE_CATALOG_SCHEMA } from './contracts.js';
3
3
  import { normalizeProviderCatalog, providerCatalogStatus, routeProviderState } from './normalize.js';
4
4
  import { compareModels, unique } from './shared.js';
5
+ import { applyBridgeModelSelection, availableModelRows, emptyBridgeModelSelection } from './model-selection.js';
5
6
  export function buildCombinedBridgeCatalog(registry, options) {
6
7
  const createdAt = options.created_at || new Date().toISOString();
7
8
  const normalized = {
8
9
  'codex-lb': normalizeProviderCatalog(options.catalogs['codex-lb']),
9
10
  openrouter: normalizeProviderCatalog(options.catalogs.openrouter)
10
11
  };
11
- const models = [...normalized['codex-lb'].models, ...normalized.openrouter.models]
12
+ const selection = options.selection || emptyBridgeModelSelection(createdAt);
13
+ const allModels = [...normalized['codex-lb'].models, ...normalized.openrouter.models]
12
14
  .sort(compareModels);
15
+ const availableOpenRouterModels = availableModelRows(allModels, selection);
16
+ const models = options.selection ? applyBridgeModelSelection(allModels, selection) : allModels;
13
17
  const routeBuild = buildBridgeRouteIndex({
14
18
  models,
15
19
  providers: {
@@ -81,6 +85,7 @@ export function buildCombinedBridgeCatalog(registry, options) {
81
85
  route_index: routeBuild.route_index,
82
86
  status,
83
87
  blockers,
84
- warnings: status.warnings
88
+ warnings: status.warnings,
89
+ available_openrouter_models: availableOpenRouterModels
85
90
  };
86
91
  }
@@ -0,0 +1,71 @@
1
+ import fsp from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { writeJsonAtomic } from '../../fsx.js';
4
+ export const BRIDGE_MODEL_SELECTION_SCHEMA = 'sks.bridge-model-selection.v1';
5
+ export const BRIDGE_MODEL_SELECTION_FILENAME = 'sks-bridge-model-selection.json';
6
+ export const BRIDGE_AVAILABLE_MODELS_FILENAME = 'sks-bridge-available-models.json';
7
+ export const MAX_SELECTED_OPENROUTER_MODELS = 64;
8
+ export function bridgeModelSelectionPath(home) {
9
+ return path.join(path.resolve(home), '.codex', 'sks', BRIDGE_MODEL_SELECTION_FILENAME);
10
+ }
11
+ export function bridgeAvailableModelsPath(home) {
12
+ return path.join(path.resolve(home), '.codex', 'sks', BRIDGE_AVAILABLE_MODELS_FILENAME);
13
+ }
14
+ export function emptyBridgeModelSelection(now) {
15
+ return { schema: BRIDGE_MODEL_SELECTION_SCHEMA, updated_at: now, openrouter: { mode: 'selected', public_ids: [] } };
16
+ }
17
+ export function normalizeBridgeModelSelection(value, now) {
18
+ if (!value || typeof value !== 'object' || Array.isArray(value))
19
+ return emptyBridgeModelSelection(now);
20
+ const row = value;
21
+ if (row.schema !== BRIDGE_MODEL_SELECTION_SCHEMA)
22
+ return emptyBridgeModelSelection(now);
23
+ const openrouter = row.openrouter && typeof row.openrouter === 'object' && !Array.isArray(row.openrouter)
24
+ ? row.openrouter
25
+ : {};
26
+ const ids = Array.isArray(openrouter.public_ids)
27
+ ? openrouter.public_ids.map((id) => String(id || '').trim()).filter(Boolean)
28
+ : [];
29
+ return {
30
+ schema: BRIDGE_MODEL_SELECTION_SCHEMA,
31
+ updated_at: typeof row.updated_at === 'string' && row.updated_at ? row.updated_at : now,
32
+ openrouter: { mode: 'selected', public_ids: [...new Set(ids)].sort().slice(0, MAX_SELECTED_OPENROUTER_MODELS) }
33
+ };
34
+ }
35
+ export async function readBridgeModelSelection(home, now = new Date().toISOString()) {
36
+ return (await readBridgeModelSelectionState(home, now)).selection;
37
+ }
38
+ export async function readBridgeModelSelectionState(home, now = new Date().toISOString()) {
39
+ try {
40
+ const raw = await fsp.readFile(bridgeModelSelectionPath(home), 'utf8');
41
+ return { selection: normalizeBridgeModelSelection(JSON.parse(raw), now), stored: true };
42
+ }
43
+ catch {
44
+ return { selection: emptyBridgeModelSelection(now), stored: false };
45
+ }
46
+ }
47
+ export async function writeBridgeModelSelection(home, selection) {
48
+ await writeJsonAtomic(bridgeModelSelectionPath(home), selection, { mode: 0o600 });
49
+ }
50
+ export function applyBridgeModelSelection(models, selection) {
51
+ const selected = new Set(selection.openrouter.public_ids);
52
+ return models.filter((model) => model.provider_id !== 'openrouter' || selected.has(model.public_id));
53
+ }
54
+ export function availableModelRows(models, selection) {
55
+ const selected = new Set(selection.openrouter.public_ids);
56
+ return models
57
+ .filter((model) => model.provider_id === 'openrouter')
58
+ .map((model) => ({
59
+ public_id: model.public_id,
60
+ display_name: model.display_name,
61
+ selected: selected.has(model.public_id)
62
+ }))
63
+ .sort((left, right) => left.display_name.localeCompare(right.display_name));
64
+ }
65
+ export function pruneSelection(selection, available) {
66
+ const known = new Set(available.map((row) => row.public_id));
67
+ const kept = selection.openrouter.public_ids.filter((id) => known.has(id));
68
+ if (kept.length === selection.openrouter.public_ids.length)
69
+ return selection;
70
+ return { ...selection, openrouter: { mode: 'selected', public_ids: kept } };
71
+ }
@@ -30,9 +30,10 @@ export function normalizeProviderCatalog(input) {
30
30
  if (!publicId || !upstreamModel)
31
31
  return null;
32
32
  const features = row?.features && typeof row.features === 'object' ? row.features : {};
33
+ const reasoning = features.reasoning === true;
33
34
  const capabilities = [
34
35
  ...(features.tools === true ? ['tools'] : []),
35
- ...(features.reasoning === true ? ['reasoning'] : []),
36
+ ...(reasoning ? ['reasoning'] : []),
36
37
  ...(features.vision === true ? ['vision'] : []),
37
38
  ...(features.audio === true ? ['audio'] : [])
38
39
  ];
@@ -45,7 +46,11 @@ export function normalizeProviderCatalog(input) {
45
46
  capabilities,
46
47
  source_catalog_generation: input.generation || 'unknown',
47
48
  route_key: `openrouter:${publicId}`,
48
- ...codexModelInfoFields(publicId, null, { tools: capabilities.includes('tools') })
49
+ ...(reasoning ? { default_reasoning_level: OPENROUTER_DEFAULT_REASONING_LEVEL } : {}),
50
+ ...codexModelInfoFields(publicId, {
51
+ supported_reasoning_levels: reasoning ? openRouterReasoningLevels() : [],
52
+ supports_reasoning_summary_parameter: reasoning
53
+ }, { tools: capabilities.includes('tools') })
49
54
  });
50
55
  }).filter(isModel);
51
56
  if (models.length === 0)
@@ -113,6 +118,7 @@ function normalizeCodexLbBridgeCatalogModels(payload, sourceCatalogGeneration) {
113
118
  ...(row.supports_audio === true || row.audio === true ? ['audio'] : [])
114
119
  ];
115
120
  models.push({
121
+ ...row,
116
122
  public_id: publicId,
117
123
  provider_id: 'codex-lb',
118
124
  upstream_model: publicId,
@@ -151,6 +157,16 @@ function sanitizeCodexLbModelRow(row, index) {
151
157
  function isPlainObject(value) {
152
158
  return Boolean(value && typeof value === 'object' && !Array.isArray(value));
153
159
  }
160
+ export const OPENROUTER_REASONING_LEVELS = [
161
+ { effort: 'low', description: 'Fast responses with lighter reasoning' },
162
+ { effort: 'medium', description: 'Balances speed and reasoning depth for everyday tasks' },
163
+ { effort: 'high', description: 'Greater reasoning depth for complex problems' },
164
+ { effort: 'xhigh', description: 'Extra high reasoning depth for complex problems' }
165
+ ];
166
+ export const OPENROUTER_DEFAULT_REASONING_LEVEL = 'medium';
167
+ function openRouterReasoningLevels() {
168
+ return OPENROUTER_REASONING_LEVELS.map((level) => ({ ...level }));
169
+ }
154
170
  export function codexModelInfoFields(slug, row, fallback) {
155
171
  const summaries = typeof row?.supports_reasoning_summary_parameter === 'boolean'
156
172
  ? row.supports_reasoning_summary_parameter
@@ -158,7 +174,7 @@ export function codexModelInfoFields(slug, row, fallback) {
158
174
  return {
159
175
  slug,
160
176
  supported_reasoning_levels: Array.isArray(row?.supported_reasoning_levels)
161
- ? row.supported_reasoning_levels.map(String)
177
+ ? row.supported_reasoning_levels.map((level) => (level && typeof level === 'object' ? level : String(level)))
162
178
  : [],
163
179
  shell_type: typeof row?.shell_type === 'string' && row.shell_type ? row.shell_type : 'shell_command',
164
180
  visibility: typeof row?.visibility === 'string' && row.visibility ? row.visibility : 'list',
@@ -186,6 +202,7 @@ function canonicalModel(model) {
186
202
  if (!publicId || !upstream)
187
203
  return null;
188
204
  return {
205
+ ...model,
189
206
  public_id: publicId,
190
207
  provider_id: model.provider_id,
191
208
  upstream_model: upstream,