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
@@ -1,4 +1,6 @@
1
- import { readText } from '../../fsx.js';
1
+ import { readText, writeJsonAtomic } from '../../fsx.js';
2
+ import { readTopLevelTomlString } from '../../codex-app/codex-model-catalog.js';
3
+ import { BRIDGE_MODEL_SELECTION_SCHEMA, MAX_SELECTED_OPENROUTER_MODELS, bridgeAvailableModelsPath, pruneSelection, readBridgeModelSelectionState, writeBridgeModelSelection } from '../combined-catalog/model-selection.js';
2
4
  import { listOpenRouterModels } from '../../providers/openrouter/openrouter-account.js';
3
5
  import { buildCombinedBridgeCatalog, readActiveCombinedBridgeCatalog, stageCombinedBridgeCatalog } from '../combined-catalog.js';
4
6
  import { codexLbEnvPath, codexLbMetadataPath, loadCodexLbEnv, readCodexLbModelCatalog } from '../codex-lb-env.js';
@@ -40,7 +42,21 @@ export async function syncCatalogInternal(options) {
40
42
  const catalogs = await fetchProviderCatalogs(initialRegistry, rawCredentials, paths, options);
41
43
  const credentials = await resolveValidatedCredentials(options, paths);
42
44
  const registry = await resolveBridgeProviderRegistry({ home: paths.home, credentials, storedRegistry });
43
- const build = buildCombinedBridgeCatalog(registry, { catalogs, created_at: nowIso(options) });
45
+ const selectionState = await readBridgeModelSelectionState(paths.home, nowIso(options));
46
+ const selection = selectionState.stored
47
+ ? selectionState.selection
48
+ : seedSelectionFromConfiguredModel(selectionState.selection, await readText(paths.configPath, ''));
49
+ const build = buildCombinedBridgeCatalog(registry, { catalogs, created_at: nowIso(options), selection });
50
+ if (!selectionState.stored)
51
+ await writeBridgeModelSelection(paths.home, selection).catch(() => undefined);
52
+ await writeJsonAtomic(bridgeAvailableModelsPath(paths.home), {
53
+ schema: 'sks.bridge-available-models.v1',
54
+ updated_at: nowIso(options),
55
+ openrouter: build.available_openrouter_models
56
+ }, { mode: 0o600 }).catch(() => undefined);
57
+ const pruned = pruneSelection(selection, build.available_openrouter_models);
58
+ if (pruned !== selection)
59
+ await writeBridgeModelSelection(paths.home, pruned).catch(() => undefined);
44
60
  const staging = await stageCombinedBridgeCatalog({
45
61
  build,
46
62
  catalogPath: paths.catalogPath,
@@ -200,11 +216,12 @@ async function fetchProviderCatalogs(registry, credentials, paths, options) {
200
216
  home: paths.home,
201
217
  validationPath: paths.validationPath
202
218
  });
219
+ const codexLbModels = result.model_rows.length > 0 ? result.model_rows : result.models;
203
220
  return [providerId, {
204
221
  provider_id: providerId,
205
222
  state: result.ok ? 'verified' : 'failed',
206
- generation: result.ok ? sha256Stable({ provider_id: providerId, models: result.models }) : null,
207
- models: { models: result.models },
223
+ generation: result.ok ? sha256Stable({ provider_id: providerId, models: codexLbModels }) : null,
224
+ models: { models: codexLbModels },
208
225
  checked_at: checkedAt,
209
226
  expires_at: expiresAt,
210
227
  blockers: result.blockers,
@@ -255,3 +272,57 @@ function validationFailureState(blockers) {
255
272
  return blockers.some((blocker) => /(?:401|403|auth|rejected|invalid_key|unauthorized)/i.test(blocker))
256
273
  ? 'rejected' : 'unavailable';
257
274
  }
275
+ export async function listSelectableModels(options) {
276
+ const paths = controllerPaths(options);
277
+ const selection = (await readBridgeModelSelectionState(paths.home, nowIso(options))).selection;
278
+ const available = await readText(bridgeAvailableModelsPath(paths.home), '');
279
+ let rows = [];
280
+ try {
281
+ const parsed = available ? JSON.parse(available) : null;
282
+ if (parsed && Array.isArray(parsed.openrouter))
283
+ rows = parsed.openrouter;
284
+ }
285
+ catch {
286
+ rows = [];
287
+ }
288
+ const selected = new Set(selection.openrouter.public_ids);
289
+ const status = await desktopBridgeStatusV3(options);
290
+ return commandResult('models.list', true, status, {
291
+ codex_lb_exposure: 'all',
292
+ openrouter: {
293
+ mode: 'selected',
294
+ selected_count: selection.openrouter.public_ids.length,
295
+ max_selected: MAX_SELECTED_OPENROUTER_MODELS,
296
+ available_count: rows.length,
297
+ models: rows.map((row) => ({
298
+ public_id: String(row?.public_id || ''),
299
+ display_name: String(row?.display_name || row?.public_id || ''),
300
+ selected: selected.has(String(row?.public_id || ''))
301
+ })).filter((row) => row.public_id)
302
+ }
303
+ }, rows.length === 0 ? ['bridge_available_models_unknown'] : [], options);
304
+ }
305
+ export async function selectExposedModels(publicIds, options) {
306
+ const paths = controllerPaths(options);
307
+ const requested = [...new Set(publicIds.map((id) => String(id || '').trim()).filter(Boolean))].sort();
308
+ if (requested.length > MAX_SELECTED_OPENROUTER_MODELS) {
309
+ throw new Error('bridge_model_selection_limit_exceeded');
310
+ }
311
+ await writeBridgeModelSelection(paths.home, {
312
+ schema: BRIDGE_MODEL_SELECTION_SCHEMA,
313
+ updated_at: nowIso(options),
314
+ openrouter: { mode: 'selected', public_ids: requested }
315
+ });
316
+ const sync = await syncCatalogInternal(options);
317
+ const status = await desktopBridgeStatusV3(options);
318
+ const activation = sync.activation && typeof sync.activation === 'object'
319
+ ? sync.activation
320
+ : {};
321
+ return commandResult('models.select', sync.ok === true, status, { selected_count: requested.length, catalog_sync: sync }, sync.ok === true ? [] : stringArray(activation.blockers), options);
322
+ }
323
+ function seedSelectionFromConfiguredModel(selection, configText) {
324
+ const configured = readTopLevelTomlString(configText, 'model');
325
+ if (!configured || !configured.includes('/'))
326
+ return selection;
327
+ return { ...selection, openrouter: { mode: 'selected', public_ids: [configured] } };
328
+ }
@@ -1,4 +1,4 @@
1
- import { syncCatalog } from './desktop-controller-v3/catalog.js';
1
+ import { listSelectableModels, selectExposedModels, syncCatalog } from './desktop-controller-v3/catalog.js';
2
2
  import { explainRoute, ensureDesktopBridge, repairDesktopBridge, rollbackDesktopBridge, setDefaultProvider, unmanageDesktopBridge } from './desktop-controller-v3/lifecycle-commands.js';
3
3
  import { configureProvider, removeCredential, setProviderState, validateProvider } from './desktop-controller-v3/provider-commands.js';
4
4
  import { commandResult, safeCode } from './desktop-controller-v3/shared.js';
@@ -40,6 +40,10 @@ export async function executeDesktopBridgeCommandV3(request, options = {}) {
40
40
  if (request.operation === 'provider.remove-credential') {
41
41
  return removeCredential(request.provider_id, options);
42
42
  }
43
+ if (request.operation === 'models.list')
44
+ return listSelectableModels(options);
45
+ if (request.operation === 'models.select')
46
+ return selectExposedModels(request.public_ids, options);
43
47
  if (request.operation === 'catalog.sync')
44
48
  return syncCatalog(options);
45
49
  if (request.operation === 'catalog.status') {
@@ -0,0 +1,372 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { flag, readOption } from '../../cli/args.js';
4
+ import { ARCHITECTURE_MAP_MANIFEST_SCHEMA, ARCHITECTURE_MAP_VIEW_IDS, GLOBAL_ARCHITECTURE_MAP_VIEW_IDS } from '../triwiki/context-graph/architecture/contracts.js';
5
+ import { ARCHITECTURE_MAP_DIR_REL } from '../triwiki/context-graph/store/architecture-map-store.js';
6
+ export const TRIWIKI_ATLAS_SUBCOMMANDS = [
7
+ 'atlas-status',
8
+ 'atlas-lint',
9
+ 'atlas-list',
10
+ 'atlas-show',
11
+ 'atlas-why'
12
+ ];
13
+ const REPAIR_COMMAND = 'sks align run';
14
+ const VIEW_ID_SET = new Set(ARCHITECTURE_MAP_VIEW_IDS);
15
+ const GLOBAL_VIEW_ID_SET = new Set(GLOBAL_ARCHITECTURE_MAP_VIEW_IDS);
16
+ export function isTriWikiAtlasSubcommand(value) {
17
+ return typeof value === 'string' && TRIWIKI_ATLAS_SUBCOMMANDS.includes(value);
18
+ }
19
+ function mapRoot(root) {
20
+ return path.join(root, ...ARCHITECTURE_MAP_DIR_REL.split('/'));
21
+ }
22
+ function missionMapRoot(root, missionId) {
23
+ return path.join(root, '.sneakoscope', 'missions', missionId, 'architecture-map');
24
+ }
25
+ function resolveMapDir(root, args) {
26
+ const missionId = readOption(args, '--mission', '') || null;
27
+ if (missionId) {
28
+ return { dir: missionMapRoot(root, missionId), mode: 'mission', missionId };
29
+ }
30
+ return { dir: mapRoot(root), mode: 'global', missionId: null };
31
+ }
32
+ function readJsonFile(file) {
33
+ try {
34
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ function loadManifest(dir) {
41
+ const file = path.join(dir, 'manifest.json');
42
+ if (!fs.existsSync(file))
43
+ return { ok: false, manifest: null, path: file };
44
+ const manifest = readJsonFile(file);
45
+ return { ok: Boolean(manifest && manifest.schema === ARCHITECTURE_MAP_MANIFEST_SCHEMA), manifest, path: file };
46
+ }
47
+ function loadFindings(dir) {
48
+ const file = path.join(dir, 'findings.json');
49
+ const doc = readJsonFile(file);
50
+ return Array.isArray(doc?.findings) ? doc.findings : [];
51
+ }
52
+ function countBySeverity(findings) {
53
+ let blocking = 0;
54
+ let warning = 0;
55
+ let info = 0;
56
+ for (const finding of findings) {
57
+ if (finding.severity === 'blocking')
58
+ blocking += 1;
59
+ else if (finding.severity === 'warning')
60
+ warning += 1;
61
+ else
62
+ info += 1;
63
+ }
64
+ return { blocking, warning, info };
65
+ }
66
+ function graphFreshness(root, manifest) {
67
+ if (!manifest)
68
+ return 'missing';
69
+ const graphPath = path.join(root, '.sneakoscope', 'wiki', 'context-graph.json');
70
+ if (!fs.existsSync(graphPath))
71
+ return 'missing';
72
+ const graph = readJsonFile(graphPath);
73
+ const expected = String(graph?.snapshotHash || '');
74
+ const observed = String(manifest.graphHash || manifest.sourceBinding?.graphHash || '');
75
+ if (!expected || !observed)
76
+ return 'missing';
77
+ return expected === observed ? 'fresh' : 'stale';
78
+ }
79
+ async function atlasStatus(root, args) {
80
+ const resolved = resolveMapDir(root, args);
81
+ const loaded = loadManifest(resolved.dir);
82
+ const findings = loaded.manifest ? loadFindings(resolved.dir) : [];
83
+ const severity = countBySeverity(findings);
84
+ const freshness = graphFreshness(root, loaded.manifest);
85
+ const ok = loaded.ok && freshness === 'fresh' && severity.blocking === 0;
86
+ const result = {
87
+ schema: 'sks.triwiki-atlas-status.v1',
88
+ ok,
89
+ mode: resolved.mode,
90
+ mission_id: resolved.missionId,
91
+ manifest_schema: loaded.manifest?.schema ?? null,
92
+ graph_hash: loaded.manifest?.graphHash ?? null,
93
+ policy_hash: loaded.manifest?.policyHash ?? null,
94
+ generated_from: loaded.manifest?.canonicalHash ?? null,
95
+ freshness,
96
+ view_count: loaded.manifest?.views?.length ?? 0,
97
+ blocking_count: severity.blocking,
98
+ warning_count: severity.warning,
99
+ projection_accounting: loaded.manifest?.projectionAccounting ?? null,
100
+ next_action: ok ? null : REPAIR_COMMAND,
101
+ repair_command: REPAIR_COMMAND
102
+ };
103
+ const lines = [
104
+ `Architecture map (${resolved.mode}): ${ok ? 'ok' : 'blocked'}`,
105
+ `Freshness: ${freshness}`,
106
+ `Views: ${result.view_count}; blocking=${severity.blocking} warning=${severity.warning}`,
107
+ ...(ok ? [] : [`Repair: ${REPAIR_COMMAND}`])
108
+ ];
109
+ return { result, ok, lines };
110
+ }
111
+ async function atlasLint(root, args) {
112
+ const resolved = resolveMapDir(root, args);
113
+ const loaded = loadManifest(resolved.dir);
114
+ const errors = [];
115
+ if (!loaded.manifest)
116
+ errors.push('manifest_missing');
117
+ else {
118
+ if (loaded.manifest.schema !== ARCHITECTURE_MAP_MANIFEST_SCHEMA)
119
+ errors.push('schema');
120
+ if (!loaded.manifest.graphHash)
121
+ errors.push('graph_hash');
122
+ if (!loaded.manifest.policyHash)
123
+ errors.push('policy_hash');
124
+ if (!Array.isArray(loaded.manifest.views) || loaded.manifest.views.length === 0)
125
+ errors.push('views');
126
+ if (!loaded.manifest.projectionAccounting)
127
+ errors.push('projection_accounting');
128
+ for (const view of loaded.manifest.views ?? []) {
129
+ if (!VIEW_ID_SET.has(view.viewId))
130
+ errors.push(`unknown_view:${view.viewId}`);
131
+ const viewPath = path.join(resolved.dir, 'views', `${view.viewId}.mmd`);
132
+ if (!fs.existsSync(viewPath)) {
133
+ errors.push(`missing_view_file:${view.viewId}`);
134
+ continue;
135
+ }
136
+ const text = fs.readFileSync(viewPath, 'utf8');
137
+ if (!text.startsWith('%% GENERATED BY SKS'))
138
+ errors.push(`header:${view.viewId}`);
139
+ if (!/\bflowchart (TD|LR)\b/.test(text))
140
+ errors.push(`grammar:${view.viewId}`);
141
+ if (/\/Users\/|\/home\/|C:\\\\/.test(text))
142
+ errors.push(`absolute_path:${view.viewId}`);
143
+ const rel = path.relative(resolved.dir, viewPath);
144
+ if (rel.startsWith('..') || path.isAbsolute(rel))
145
+ errors.push(`path_escape:${view.viewId}`);
146
+ }
147
+ }
148
+ const freshness = graphFreshness(root, loaded.manifest);
149
+ if (freshness !== 'fresh')
150
+ errors.push(`freshness_${freshness}`);
151
+ const ok = errors.length === 0;
152
+ const result = {
153
+ schema: 'sks.triwiki-atlas-lint.v1',
154
+ ok,
155
+ mode: resolved.mode,
156
+ mission_id: resolved.missionId,
157
+ error_count: errors.length,
158
+ errors,
159
+ repair_command: REPAIR_COMMAND
160
+ };
161
+ return {
162
+ result,
163
+ ok,
164
+ lines: [
165
+ `Architecture map lint: ${ok ? 'ok' : 'blocked'} (${errors.length} errors)`,
166
+ ...errors.slice(0, 20).map((error) => `- ${error}`),
167
+ ...(ok ? [] : [`Repair: ${REPAIR_COMMAND}`])
168
+ ]
169
+ };
170
+ }
171
+ async function atlasList(root, args) {
172
+ const resolved = resolveMapDir(root, args);
173
+ const loaded = loadManifest(resolved.dir);
174
+ if (!loaded.manifest) {
175
+ return {
176
+ result: {
177
+ schema: 'sks.triwiki-atlas-list.v1',
178
+ ok: false,
179
+ error_code: 'manifest_missing',
180
+ repair_command: REPAIR_COMMAND
181
+ },
182
+ ok: false,
183
+ lines: ['Architecture map list: manifest_missing', `Repair: ${REPAIR_COMMAND}`]
184
+ };
185
+ }
186
+ const views = (loaded.manifest.views ?? []).map((view) => {
187
+ const rel = `${ARCHITECTURE_MAP_DIR_REL}/views/${view.viewId}.mmd`;
188
+ const abs = path.join(resolved.dir, 'views', `${view.viewId}.mmd`);
189
+ let direction = null;
190
+ let nodeCount = 0;
191
+ let edgeCount = 0;
192
+ if (fs.existsSync(abs)) {
193
+ const text = fs.readFileSync(abs, 'utf8');
194
+ direction = text.match(/\bflowchart (TD|LR)\b/)?.[1] ?? null;
195
+ nodeCount = [...text.matchAll(/^\s*[A-Za-z0-9_]+\[/gm)].length;
196
+ edgeCount = [...text.matchAll(/-->/g)].length;
197
+ }
198
+ return {
199
+ view_id: view.viewId,
200
+ path: rel,
201
+ direction,
202
+ node_count: nodeCount,
203
+ edge_count: edgeCount,
204
+ byte_count: view.byteLength,
205
+ hash: view.contentHash
206
+ };
207
+ });
208
+ const result = {
209
+ schema: 'sks.triwiki-atlas-list.v1',
210
+ ok: true,
211
+ mode: resolved.mode,
212
+ mission_id: resolved.missionId,
213
+ views
214
+ };
215
+ return {
216
+ result,
217
+ ok: true,
218
+ lines: views.map((view) => `${view.view_id} ${view.direction ?? '?'} nodes=${view.node_count} edges=${view.edge_count} bytes=${view.byte_count} ${view.hash.slice(0, 12)}`)
219
+ };
220
+ }
221
+ async function atlasShow(root, args) {
222
+ const viewId = args.find((arg) => !arg.startsWith('-')) ?? '';
223
+ if (!viewId || !VIEW_ID_SET.has(viewId)) {
224
+ return {
225
+ result: {
226
+ schema: 'sks.triwiki-atlas-show.v1',
227
+ ok: false,
228
+ error_code: 'invalid_view_id',
229
+ allowed: [...ARCHITECTURE_MAP_VIEW_IDS]
230
+ },
231
+ ok: false,
232
+ lines: [
233
+ 'Usage: sks triwiki atlas-show <view-id> [--mission <id>] [--format mermaid|json]',
234
+ `Allowed: ${ARCHITECTURE_MAP_VIEW_IDS.join(', ')}`
235
+ ]
236
+ };
237
+ }
238
+ if (!GLOBAL_VIEW_ID_SET.has(viewId) && !readOption(args, '--mission', '')) {
239
+ return {
240
+ result: {
241
+ schema: 'sks.triwiki-atlas-show.v1',
242
+ ok: false,
243
+ error_code: 'mission_view_requires_mission',
244
+ view_id: viewId
245
+ },
246
+ ok: false,
247
+ lines: [`View ${viewId} is mission-scoped; pass --mission <id>`]
248
+ };
249
+ }
250
+ const format = readOption(args, '--format', 'mermaid') || 'mermaid';
251
+ const resolved = resolveMapDir(root, args);
252
+ const viewPath = path.join(resolved.dir, 'views', `${viewId}.mmd`);
253
+ const confined = path.relative(resolved.dir, viewPath);
254
+ if (confined.startsWith('..') || path.isAbsolute(confined)) {
255
+ return {
256
+ result: { schema: 'sks.triwiki-atlas-show.v1', ok: false, error_code: 'path_escape' },
257
+ ok: false,
258
+ lines: ['Architecture map show blocked: path_escape']
259
+ };
260
+ }
261
+ if (!fs.existsSync(viewPath)) {
262
+ return {
263
+ result: {
264
+ schema: 'sks.triwiki-atlas-show.v1',
265
+ ok: false,
266
+ error_code: 'view_missing',
267
+ view_id: viewId,
268
+ repair_command: REPAIR_COMMAND
269
+ },
270
+ ok: false,
271
+ lines: [`Architecture map show: view_missing (${viewId})`, `Repair: ${REPAIR_COMMAND}`]
272
+ };
273
+ }
274
+ const text = fs.readFileSync(viewPath, 'utf8');
275
+ if (format === 'json') {
276
+ const result = {
277
+ schema: 'sks.triwiki-atlas-show.v1',
278
+ ok: true,
279
+ view_id: viewId,
280
+ format: 'json',
281
+ path: `${ARCHITECTURE_MAP_DIR_REL}/views/${viewId}.mmd`,
282
+ text
283
+ };
284
+ return { result, ok: true, lines: [JSON.stringify(result, null, 2)] };
285
+ }
286
+ return {
287
+ result: {
288
+ schema: 'sks.triwiki-atlas-show.v1',
289
+ ok: true,
290
+ view_id: viewId,
291
+ format: 'mermaid',
292
+ path: `${ARCHITECTURE_MAP_DIR_REL}/views/${viewId}.mmd`,
293
+ text
294
+ },
295
+ ok: true,
296
+ lines: [text.trimEnd()]
297
+ };
298
+ }
299
+ async function atlasWhy(root, args) {
300
+ const findingId = args.find((arg) => !arg.startsWith('-')) ?? '';
301
+ const missionId = readOption(args, '--mission', '');
302
+ if (!findingId || !missionId) {
303
+ return {
304
+ result: { schema: 'sks.triwiki-atlas-why.v1', ok: false, error_code: 'usage' },
305
+ ok: false,
306
+ lines: ['Usage: sks triwiki atlas-why <finding-id> --mission <id> [--json]']
307
+ };
308
+ }
309
+ const resolved = resolveMapDir(root, [`--mission`, missionId]);
310
+ const findings = loadFindings(resolved.dir);
311
+ const finding = findings.find((entry) => entry.id === findingId) ?? null;
312
+ if (!finding) {
313
+ return {
314
+ result: {
315
+ schema: 'sks.triwiki-atlas-why.v1',
316
+ ok: false,
317
+ error_code: 'finding_not_found',
318
+ finding_id: findingId,
319
+ mission_id: missionId
320
+ },
321
+ ok: false,
322
+ lines: [`Architecture map why: finding_not_found (${findingId})`]
323
+ };
324
+ }
325
+ const result = {
326
+ schema: 'sks.triwiki-atlas-why.v1',
327
+ ok: true,
328
+ finding_id: finding.id,
329
+ code: finding.code ?? null,
330
+ severity: finding.severity ?? null,
331
+ subject: finding.subjectIds ?? [],
332
+ evidence_path: finding.evidenceIds ?? [],
333
+ before: null,
334
+ after: null,
335
+ policy_rule: finding.ruleId ?? null,
336
+ exception: finding.disposition ?? null,
337
+ inspect_nodes: finding.subjectIds ?? [],
338
+ message: finding.message ?? null
339
+ };
340
+ return {
341
+ result,
342
+ ok: true,
343
+ lines: [
344
+ `${finding.id}: ${finding.code ?? 'unknown'} (${finding.severity ?? 'info'})`,
345
+ ...(finding.subjectIds?.length ? [`Subject: ${finding.subjectIds.join(', ')}`] : []),
346
+ ...(finding.message ? [finding.message] : [])
347
+ ]
348
+ };
349
+ }
350
+ export async function triwikiAtlasCommand(root, sub, args) {
351
+ let answer;
352
+ if (sub === 'atlas-status')
353
+ answer = await atlasStatus(root, args);
354
+ else if (sub === 'atlas-lint')
355
+ answer = await atlasLint(root, args);
356
+ else if (sub === 'atlas-list')
357
+ answer = await atlasList(root, args);
358
+ else if (sub === 'atlas-show')
359
+ answer = await atlasShow(root, args);
360
+ else
361
+ answer = await atlasWhy(root, args);
362
+ const usageFailed = typeof answer.result === 'object' &&
363
+ answer.result !== null &&
364
+ 'error_code' in answer.result &&
365
+ answer.result.error_code === 'usage';
366
+ const invalidView = typeof answer.result === 'object' &&
367
+ answer.result !== null &&
368
+ 'error_code' in answer.result &&
369
+ answer.result.error_code === 'invalid_view_id';
370
+ const exitCode = answer.ok ? 0 : usageFailed || invalidView ? 2 : 1;
371
+ return { ...answer, exitCode };
372
+ }
@@ -5,6 +5,7 @@ import { computeTriWikiAffectedGraph } from '../triwiki/triwiki-affected-graph.j
5
5
  import { buildTriWikiGateImpactMap } from '../triwiki/triwiki-gate-impact-map.js';
6
6
  import { DEFAULT_TRIWIKI_MODULE_CARDS } from '../triwiki/triwiki-module-card.js';
7
7
  import { summarizeTriWikiProofBank } from '../triwiki/triwiki-proof-bank.js';
8
+ import { isTriWikiAtlasSubcommand, triwikiAtlasCommand } from './triwiki-atlas-command.js';
8
9
  import { isTriWikiGraphSubcommand, triwikiGraphCommand } from './triwiki-graph-command.js';
9
10
  export async function triwikiCommand(args = []) {
10
11
  const root = await projectRoot();
@@ -20,6 +21,16 @@ export async function triwikiCommand(args = []) {
20
21
  console.log(line);
21
22
  return answer.result;
22
23
  }
24
+ if (isTriWikiAtlasSubcommand(sub)) {
25
+ const answer = await triwikiAtlasCommand(root, sub, args.slice(1));
26
+ process.exitCode = answer.exitCode;
27
+ if (json)
28
+ return printJson(answer.result);
29
+ const stream = answer.ok ? console.log : console.error;
30
+ for (const line of answer.lines)
31
+ stream(line);
32
+ return answer.result;
33
+ }
23
34
  if (sub === 'index') {
24
35
  result = {
25
36
  schema: 'sks.triwiki-index.v1',
@@ -36,7 +47,7 @@ export async function triwikiCommand(args = []) {
36
47
  result = summarizeTriWikiProofBank(root);
37
48
  }
38
49
  else {
39
- console.error('Usage: sks triwiki index|affected|proof-bank|graph-status|graph-lint|graph-query [--json]');
50
+ console.error('Usage: sks triwiki index|affected|proof-bank|graph-status|graph-lint|graph-query|atlas-status|atlas-lint|atlas-list|atlas-show|atlas-why [--json]');
40
51
  process.exitCode = 1;
41
52
  return null;
42
53
  }
package/dist/core/fsx.js CHANGED
@@ -65,6 +65,20 @@ export async function canonicalFilesystemPath(p) {
65
65
  const resolved = path.resolve(p);
66
66
  return fsp.realpath(resolved).catch(() => resolved);
67
67
  }
68
+ export function canonicalFilesystemPathSync(value) {
69
+ const resolved = path.resolve(value);
70
+ try {
71
+ return fs.realpathSync.native(resolved);
72
+ }
73
+ catch {
74
+ if (process.platform === 'darwin' && resolved.startsWith('/var/'))
75
+ return `/private${resolved}`;
76
+ return resolved;
77
+ }
78
+ }
79
+ export function sameFilesystemPathSync(left, right) {
80
+ return Boolean(left && right && canonicalFilesystemPathSync(left) === canonicalFilesystemPathSync(right));
81
+ }
68
82
  export async function sameFilesystemPath(left, right) {
69
83
  const [leftCanonical, rightCanonical] = await Promise.all([
70
84
  canonicalFilesystemPath(left),
@@ -17,6 +17,8 @@ export const SHARED_MEMORY_TRACK = [
17
17
  export const GENERATED_INDEX_IGNORE = [
18
18
  '.sneakoscope/wiki/indexes/**/*.json',
19
19
  '.sneakoscope/wiki/context-packs/**/*.json',
20
+ '.sneakoscope/wiki/architecture-map/**',
21
+ '.sneakoscope/wiki/architecture-map/**/*.mmd',
20
22
  '.sneakoscope/wiki/tmp/**'
21
23
  ];
22
24
  export const LOCAL_RUNTIME_IGNORE = [
@@ -53,6 +55,8 @@ export const LOCAL_RUNTIME_IGNORE = [
53
55
  '.sneakoscope/wiki/image-wrongness-index.json',
54
56
  '.sneakoscope/wiki/visual-anchors.json',
55
57
  '.sneakoscope/wiki/last-sweep-report.json',
58
+ '.sneakoscope/wiki/architecture-map/**',
59
+ '.sneakoscope/wiki/architecture-map/**/*.mmd',
56
60
  '.sneakoscope/**/*.log'
57
61
  ];
58
62
  export const SHARED_MEMORY_DIRS = [
@@ -32,6 +32,8 @@ export const GITIGNORE_BLOCK = `# BEGIN ${GIT_HYGIENE_BLOCK}
32
32
  # Generated TriWiki indexes and transient context packs
33
33
  .sneakoscope/wiki/indexes/
34
34
  .sneakoscope/wiki/context-packs/
35
+ .sneakoscope/wiki/architecture-map/
36
+ .sneakoscope/wiki/architecture-map/**
35
37
  .sneakoscope/wiki/tmp/
36
38
  .sneakoscope/wiki/context-pack.json
37
39
  .sneakoscope/wiki/image-assets.json
@@ -78,7 +78,8 @@ export function normalizeVGraph(vgraph = {}) {
78
78
  edges,
79
79
  invariants: Array.isArray(vgraph.invariants) ? vgraph.invariants : [],
80
80
  tests: Array.isArray(vgraph.tests) ? vgraph.tests : [],
81
- risks: Array.isArray(vgraph.risks) ? vgraph.risks : []
81
+ risks: Array.isArray(vgraph.risks) ? vgraph.risks : [],
82
+ ...(vgraph.atlas && typeof vgraph.atlas === 'object' && !Array.isArray(vgraph.atlas) ? { atlas: vgraph.atlas } : {})
82
83
  };
83
84
  }
84
85
  export function vgraphHash(vgraph = {}) {
@@ -29,6 +29,7 @@ export function defaultManagedPaths() {
29
29
  row('.sneakoscope/wiki/summaries', 'tracked shared markdown summaries', false, 'shared_memory', 'tracked'),
30
30
  row('.sneakoscope/wiki/indexes', 'ignored regenerated shared-memory indexes', true, 'generated_index', 'ignored'),
31
31
  row('.sneakoscope/wiki/context-packs', 'ignored regenerated context-pack snapshots', true, 'generated_index', 'ignored'),
32
+ row('.sneakoscope/wiki/architecture-map', 'ignored regenerated Architecture Map views', true, 'generated_index', 'ignored'),
32
33
  row('.sneakoscope/missions', 'local runtime mission state and proof artifacts', true, 'local_runtime', 'ignored'),
33
34
  row('.sneakoscope/reports', 'local runtime reports', true, 'local_runtime', 'ignored'),
34
35
  row('.sneakoscope/tmp', 'local temporary runtime files', true, 'local_runtime', 'ignored'),