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.
Files changed (122) 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 +97 -16
  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 +173 -0
  12. package/dist/core/architecture-map-review.js +180 -0
  13. package/dist/core/codex/agent-config-file-repair.js +17 -3
  14. package/dist/core/codex/codex-config-guard.js +3 -1
  15. package/dist/core/codex/codex-feature-flags.js +10 -0
  16. package/dist/core/codex/codex-project-config-policy.js +54 -30
  17. package/dist/core/codex/codex-startup-config-postcheck.js +5 -0
  18. package/dist/core/codex-app/codex-app-fast-ui-repair.js +2 -1
  19. package/dist/core/codex-app/menubar/constants.js +1 -0
  20. package/dist/core/codex-hooks/codex-hook-state-writer.js +2 -0
  21. package/dist/core/codex-lb/codex-lb-env.js +25 -3
  22. package/dist/core/codex-lb/combined-catalog/builder.js +7 -2
  23. package/dist/core/codex-lb/combined-catalog/model-selection.js +71 -0
  24. package/dist/core/codex-lb/combined-catalog/normalize.js +20 -3
  25. package/dist/core/codex-lb/desktop-bridge/security.js +11 -8
  26. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +75 -4
  27. package/dist/core/codex-lb/desktop-controller-v3.js +5 -1
  28. package/dist/core/codex-lb/request-route-resolver.js +21 -18
  29. package/dist/core/commands/triwiki-atlas-command.js +372 -0
  30. package/dist/core/commands/triwiki-command.js +12 -1
  31. package/dist/core/doctor/doctor-dirty-planner.js +16 -1
  32. package/dist/core/doctor/doctor-idempotence.js +11 -0
  33. package/dist/core/doctor/doctor-repair-postcheck.js +49 -0
  34. package/dist/core/doctor/doctor-transaction.js +47 -2
  35. package/dist/core/doctor/legacy-global-hook-cleanup.js +1 -1
  36. package/dist/core/fsx.js +14 -0
  37. package/dist/core/git-hygiene/git-policy.js +4 -0
  38. package/dist/core/git-hygiene/gitignore-writer.js +2 -0
  39. package/dist/core/gx-renderer.js +2 -1
  40. package/dist/core/init.js +40 -17
  41. package/dist/core/managed-paths.js +1 -0
  42. package/dist/core/perf/release-latency-slo.js +57 -11
  43. package/dist/core/pipeline-internals/pipeline-stage-builder.js +181 -0
  44. package/dist/core/pipeline-internals/runtime-core.js +18 -172
  45. package/dist/core/pipeline-internals/runtime-gates.js +12 -1
  46. package/dist/core/release/content-digest-cache.js +70 -0
  47. package/dist/core/release/gate-manifest.js +9 -0
  48. package/dist/core/release/main-push-guard.js +4 -3
  49. package/dist/core/release/package-dist-snapshot.js +18 -2
  50. package/dist/core/release/physical-release-gates.js +26 -3
  51. package/dist/core/release/release-authorization-snapshot.js +4 -0
  52. package/dist/core/release/release-gate-cache-v2.js +27 -3
  53. package/dist/core/release/release-gate-contract.js +2 -0
  54. package/dist/core/release/release-gate-dag.js +3 -0
  55. package/dist/core/release/release-inspection-memo.js +69 -0
  56. package/dist/core/release/release-pack-content-scanner.js +5 -0
  57. package/dist/core/release/release-pack-receipt.js +28 -18
  58. package/dist/core/release/release-pack-tarball.js +10 -0
  59. package/dist/core/release/release-upgrade-baseline.js +4 -0
  60. package/dist/core/routes.js +1 -1
  61. package/dist/core/safety/ssot-authority-inventory.js +114 -0
  62. package/dist/core/safety/ssot-guard.js +2 -44
  63. package/dist/core/subagents/agent-catalog.js +4 -2
  64. package/dist/core/subagents/official-subagent-config.js +92 -15
  65. package/dist/core/subagents/official-subagent-preparation.js +1 -0
  66. package/dist/core/triwiki/context-graph/architecture/baseline.js +81 -0
  67. package/dist/core/triwiki/context-graph/architecture/capsule.js +34 -0
  68. package/dist/core/triwiki/context-graph/architecture/contracts.js +67 -0
  69. package/dist/core/triwiki/context-graph/architecture/delta.js +149 -0
  70. package/dist/core/triwiki/context-graph/architecture/finding-factory.js +28 -0
  71. package/dist/core/triwiki/context-graph/architecture/findings.js +173 -0
  72. package/dist/core/triwiki/context-graph/architecture/fingerprint.js +75 -0
  73. package/dist/core/triwiki/context-graph/architecture/index.js +15 -0
  74. package/dist/core/triwiki/context-graph/architecture/input-bundle.js +66 -0
  75. package/dist/core/triwiki/context-graph/architecture/metrics.js +137 -0
  76. package/dist/core/triwiki/context-graph/architecture/policy.js +321 -0
  77. package/dist/core/triwiki/context-graph/architecture/slice.js +100 -0
  78. package/dist/core/triwiki/context-graph/architecture/ssot-analysis.js +59 -0
  79. package/dist/core/triwiki/context-graph/architecture/topology-overlay.js +106 -0
  80. package/dist/core/triwiki/context-graph/architecture/validation.js +145 -0
  81. package/dist/core/triwiki/context-graph/architecture/voxel-context.js +39 -0
  82. package/dist/core/triwiki/context-graph/extractors/index.js +4 -0
  83. package/dist/core/triwiki/context-graph/projections/index.js +2 -0
  84. package/dist/core/triwiki/context-graph/projections/mermaid/architecture-delta.js +68 -0
  85. package/dist/core/triwiki/context-graph/projections/mermaid/ast.js +49 -0
  86. package/dist/core/triwiki/context-graph/projections/mermaid/change-impact.js +43 -0
  87. package/dist/core/triwiki/context-graph/projections/mermaid/contracts.js +5 -0
  88. package/dist/core/triwiki/context-graph/projections/mermaid/escape.js +45 -0
  89. package/dist/core/triwiki/context-graph/projections/mermaid/ids.js +73 -0
  90. package/dist/core/triwiki/context-graph/projections/mermaid/index.js +113 -0
  91. package/dist/core/triwiki/context-graph/projections/mermaid/manifest.js +37 -0
  92. package/dist/core/triwiki/context-graph/projections/mermaid/module-dependency.js +21 -0
  93. package/dist/core/triwiki/context-graph/projections/mermaid/ownership-workstream.js +21 -0
  94. package/dist/core/triwiki/context-graph/projections/mermaid/project-topology.js +13 -0
  95. package/dist/core/triwiki/context-graph/projections/mermaid/public-surface.js +17 -0
  96. package/dist/core/triwiki/context-graph/projections/mermaid/reduce.js +44 -0
  97. package/dist/core/triwiki/context-graph/projections/mermaid/risk-domains.js +23 -0
  98. package/dist/core/triwiki/context-graph/projections/mermaid/runtime-control.js +20 -0
  99. package/dist/core/triwiki/context-graph/projections/mermaid/selection-ledger.js +94 -0
  100. package/dist/core/triwiki/context-graph/projections/mermaid/serializer.js +81 -0
  101. package/dist/core/triwiki/context-graph/projections/mermaid/ssot-provenance.js +17 -0
  102. package/dist/core/triwiki/context-graph/projections/mermaid/verification-coverage.js +17 -0
  103. package/dist/core/triwiki/context-graph/projections/mermaid/view-builder-common.js +1 -0
  104. package/dist/core/triwiki/context-graph/projections/mermaid/view-builder.js +173 -0
  105. package/dist/core/triwiki/context-graph/projections/mermaid/view-specs.js +33 -0
  106. package/dist/core/triwiki/context-graph/store/architecture-map-store.js +71 -0
  107. package/dist/core/triwiki/triwiki-proof-bank-index.js +26 -1
  108. package/dist/core/triwiki/triwiki-proof-bank.js +82 -2
  109. package/dist/core/version.js +1 -1
  110. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +2 -2
  111. package/dist/native/sks-menubar/Sources/ProvidersModelExposure.swift +178 -0
  112. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +10 -1
  113. package/dist/scripts/architecture-map-check.js +299 -0
  114. package/dist/scripts/lib/ensure-dist-fresh.js +48 -1
  115. package/dist/scripts/release-metadata-check.js +2 -0
  116. package/package.json +6 -3
  117. package/release-gates.v2.json +204 -0
  118. package/schemas/architecture-baseline.v1.schema.json +47 -0
  119. package/schemas/architecture-map-manifest.v1.schema.json +40 -0
  120. package/schemas/architecture-map-policy.v1.schema.json +147 -0
  121. package/schemas/architecture-review.v1.schema.json +46 -0
  122. package/schemas/mermaid-projection.v1.schema.json +47 -0
@@ -38,7 +38,10 @@ import { NARUTO_GATE_FILENAME, NARUTO_SUMMARY_FILENAME, SUBAGENT_PLAN_FILENAME,
38
38
  import { buildSubagentEvidence, readSubagentEvents, recordSubagentEvent, SUBAGENT_EVIDENCE_FILENAME, SUBAGENT_EVENT_LOG_FILENAME, SUBAGENT_PARENT_SUMMARY_FILENAME } from '../subagents/subagent-evidence.js';
39
39
  import { effectiveSubagentTarget, normalizeLegacySubagentCountFields, subagentCountContractBlockers } from '../subagents/wave-lifecycle.js';
40
40
  import { officialSubagentEvidenceReady } from '../subagents/terminal-subagent-state.js';
41
+ import { createArchitectureMapPlanBinding, maybeSeedArchitectureMapForPlan, planStagesArchitectureMap } from '../architecture-map-pipeline.js';
42
+ import { BLOCKING_GATE_LIMITS, LIGHTWEIGHT_ROUTES, GATE_PROFILE_STAGES, STAGE_BLOCKING_GATE, selectPipelineLane, buildPipelineStages, planVerification, pipelineInvariants } from './pipeline-stage-builder.js';
41
43
  export { routePrompt };
44
+ export { LIGHTWEIGHT_ROUTES, GATE_PROFILE_STAGES, STAGE_BLOCKING_GATE, BLOCKING_GATE_LIMITS };
42
45
  export const PIPELINE_PLAN_ARTIFACT = 'pipeline-plan.json';
43
46
  export const PIPELINE_PLAN_SCHEMA_VERSION = 1;
44
47
  const SKILL_DREAM_HOT_PATH_COUNTERS = new Map();
@@ -60,44 +63,6 @@ const HARD_BLOCKER_ARTIFACT = 'hard-blocker.json';
60
63
  const DEFAULT_COMPLIANCE_LOOP_LIMIT = 3;
61
64
  const CLARIFICATION_BYPASS_ROUTES = new Set(['Answer', 'DFix', 'Help', 'Wiki', 'ComputerUse', 'Goal']);
62
65
  const QUESTION_GATE_ROUTES = new Set(['QALoop', 'PPT']);
63
- const LIGHTWEIGHT_ROUTES = new Set(['Answer', 'DFix', 'Help', 'Wiki', 'Goal']);
64
- const GATE_PROFILE_STAGES = Object.freeze({
65
- none: [],
66
- minimal: ['route_classification', 'listed_verification'],
67
- scoped: ['route_classification', 'ownership', 'listed_verification', 'honest_summary'],
68
- full: ['route_classification', 'ambiguity_gate', 'safety_gate', 'ownership', 'listed_verification', 'honest_summary']
69
- });
70
- const STAGE_BLOCKING_GATE = Object.freeze({
71
- ambiguity_gate: 'scope',
72
- safety_gate: 'safety',
73
- ssot_guard: 'safety',
74
- context7_evidence: 'safety',
75
- mistake_recall: 'safety',
76
- ownership: 'ownership',
77
- pipeline_plan: 'ownership',
78
- focused_implementation: 'ownership',
79
- triwiki_use_first: 'ownership',
80
- subagent_plan: 'ownership',
81
- official_subagent_execution: 'ownership',
82
- parent_integration: 'ownership',
83
- route_materialization: 'ownership',
84
- work_order_coverage: 'ownership',
85
- engineering_sanity_check: 'verification',
86
- listed_verification: 'verification',
87
- triwiki_validate_before_final: 'verification',
88
- completion_proof: 'verification',
89
- reflection: 'verification',
90
- honest_summary: 'verification'
91
- });
92
- const BLOCKING_GATE_LIMITS = Object.freeze({
93
- passthrough: 0,
94
- answer: 0,
95
- 'tiny-change': 1,
96
- 'bounded-work': 2,
97
- 'parallel-read': 2,
98
- 'parallel-write': 3,
99
- 'high-risk': 4
100
- });
101
66
  function reflectionInstructionText(commandPrefix = 'sks') {
102
67
  return `Post-route reflection: full routes load \`reflection\` after work/tests and before final; DFix/Answer/Help/Wiki/SKS discovery are exempt. Write ${REFLECTION_ARTIFACT}; record only real misses/gaps, or no_issue_acknowledged. For lessons, append TriWiki claim rows to ${REFLECTION_MEMORY_PATH}. Run "${commandPrefix} wiki refresh" or pack, validate, then pass ${REFLECTION_GATE}.`;
103
68
  }
@@ -182,6 +147,8 @@ export function buildPipelinePlan(input = {}) {
182
147
  verification_budget: verificationBudget,
183
148
  verification,
184
149
  engineering_sanity_review: null,
150
+ architecture_map: null,
151
+ architecture_map_unavailable: null,
185
152
  invariants,
186
153
  proof_field: proof,
187
154
  ssot_guard: buildSsotGuard({ route: route?.id || 'SKS', mode: route?.mode || 'SKS', task }),
@@ -219,6 +186,16 @@ export async function writePipelinePlan(dir, input = {}) {
219
186
  changed_scope_base: resolveChangedScopeBase(root)
220
187
  };
221
188
  }
189
+ if (planStagesArchitectureMap(plan)) {
190
+ plan.architecture_map = createArchitectureMapPlanBinding();
191
+ const seeded = await maybeSeedArchitectureMapForPlan({
192
+ root, dir, plan, missionId: input.missionId, taskProfile
193
+ });
194
+ if (seeded && seeded.ok === false) {
195
+ plan.architecture_map = null;
196
+ plan.architecture_map_unavailable = { reason: seeded.reason || 'architecture_map_baseline_unavailable' };
197
+ }
198
+ }
222
199
  await writeJsonAtomic(path.join(dir, PIPELINE_PLAN_ARTIFACT), plan);
223
200
  if (plan.engineering_sanity_review) {
224
201
  const report = await writeCodeStructureReport(root, dir, {
@@ -244,7 +221,8 @@ export function pipelinePlanState(plan) {
244
221
  pipeline_plan_ready: validatePipelinePlan(plan).ok,
245
222
  pipeline_plan_path: PIPELINE_PLAN_ARTIFACT,
246
223
  engineering_sanity_required: Boolean(sanity),
247
- engineering_sanity_scope_base: sanity?.changed_scope_base || null
224
+ engineering_sanity_scope_base: sanity?.changed_scope_base || null,
225
+ architecture_map_required: Boolean(plan?.architecture_map)
248
226
  };
249
227
  }
250
228
  export async function writeRequestIntakeArtifact(dir, input = {}) {
@@ -488,130 +466,6 @@ function routeEconomyPlan(proof = {}) {
488
466
  deletion_policy: 'do_not_delete_or_skip_pipeline_stages_until_report_only_metrics_are_calibrated'
489
467
  };
490
468
  }
491
- function selectPipelineLane(route, task, proof, taskProfile = classifyTaskProfile(task)) {
492
- if (proof.attached && proof.lane) {
493
- return {
494
- lane: proof.lane,
495
- source: 'proof_field',
496
- fast_lane_allowed: Boolean(proof.fast_lane_allowed),
497
- reason: proof.fast_lane_allowed ? 'Proof Field allowed the fast lane.' : `Proof Field selected ${proof.lane}.`,
498
- blockers: proof.blockers || [],
499
- skip_when_fast: proof.fast_lane_allowed ? SPEED_LANE_POLICY.skip_when_fast : [],
500
- keep: proof.keep || SPEED_LANE_POLICY.always_keep
501
- };
502
- }
503
- if (taskProfile === 'passthrough' || taskProfile === 'answer')
504
- return { lane: 'no_pipeline', source: 'task_profile', fast_lane_allowed: true, reason: 'Light conversation does not create an execution pipeline.', blockers: [], skip_when_fast: [], keep: [] };
505
- if (route?.id === 'ComputerUse')
506
- return { lane: 'computer_use_fast_lane', source: 'route_policy', fast_lane_allowed: true, reason: 'Computer Use route is intentionally direct and defers wiki/honest checks to closeout.', blockers: [], skip_when_fast: ['planning_debate'], keep: ['focused_implementation', 'triwiki_validate_before_final', 'honest_mode'] };
507
- if (taskProfile === 'tiny-change')
508
- return { lane: 'minimal_change_lane', source: 'task_profile', fast_lane_allowed: true, reason: 'Tiny change uses one blocking gate and one focused check at most.', blockers: [], skip_when_fast: SPEED_LANE_POLICY.skip_when_fast, keep: ['listed_verification'] };
509
- if (LIGHTWEIGHT_ROUTES.has(route?.id))
510
- return { lane: `${String(route.id).toLowerCase()}_lightweight_lane`, source: 'route_policy', fast_lane_allowed: true, reason: 'Lightweight route bypasses full mission orchestration by design.', blockers: [], skip_when_fast: SPEED_LANE_POLICY.skip_when_fast, keep: ['focused_implementation', 'listed_verification', 'honest_mode'] };
511
- if (routeRequiresSubagents(route, task, taskProfile))
512
- return { lane: 'official_subagent_lane', source: 'task_profile', fast_lane_allowed: false, reason: 'Explicit Naruto or parallel work uses the Codex subagent workflow.', blockers: [], skip_when_fast: [], keep: ['subagent_plan', 'official_subagent_execution', 'parent_integration', 'listed_verification', 'honest_summary'] };
513
- if (taskProfile === 'high-risk')
514
- return { lane: SPEED_LANE_POLICY.full_lane, source: 'task_profile', fast_lane_allowed: false, reason: 'High-risk work uses the full risk gate profile.', blockers: [], skip_when_fast: [], keep: SPEED_LANE_POLICY.always_keep };
515
- return { lane: SPEED_LANE_POLICY.balanced_lane, source: 'route_policy', fast_lane_allowed: false, reason: 'Balanced parent-owned route until Proof Field proves a narrower lane.', blockers: ['proof_field_not_attached'], skip_when_fast: [], keep: SPEED_LANE_POLICY.always_keep };
516
- }
517
- function buildPipelineStages(route, task, taskProfile, gateProfile, ambiguity, lane, context7Required, officialSubagentPolicy = normalizeOfficialSubagentPolicy(route, task, {})) {
518
- if (gateProfile === 'none')
519
- return [];
520
- const ids = [...GATE_PROFILE_STAGES[gateProfile]];
521
- const specializedRoute = Boolean(route?.id && !LIGHTWEIGHT_ROUTES.has(route.id) && route.id !== 'SKS');
522
- if (gateProfile === 'scoped' || gateProfile === 'full' || specializedRoute)
523
- ids.push('pipeline_plan', 'focused_implementation');
524
- if ((gateProfile === 'full' || specializedRoute) && !ids.includes('ssot_guard'))
525
- ids.push('ssot_guard');
526
- if (context7Required)
527
- ids.push('context7_evidence');
528
- if ((gateProfile === 'scoped' || gateProfile === 'full') && !LIGHTWEIGHT_ROUTES.has(route?.id)) {
529
- ids.push('triwiki_use_first', 'triwiki_validate_before_final', 'mistake_recall', 'work_order_coverage');
530
- }
531
- if (routeNeedsEngineeringSanityReview(route, task))
532
- ids.push('engineering_sanity_check');
533
- if (routeRequiresSubagents(route, task, taskProfile))
534
- ids.push('subagent_plan', 'official_subagent_execution', 'parent_integration');
535
- if (specializedRoute)
536
- ids.push('route_materialization');
537
- if (specializedRoute)
538
- ids.push('completion_proof');
539
- if (reflectionRequiredForRoute(route))
540
- ids.push('reflection');
541
- return [...new Set(ids)].map((id) => {
542
- const configuredGate = STAGE_BLOCKING_GATE[id] || null;
543
- const blockingGate = configuredGate === 'safety' && gateProfile !== 'full'
544
- ? 'ownership'
545
- : configuredGate;
546
- const blocking = Boolean(blockingGate);
547
- const metadata = { blocking, blocking_gate: blocking ? blockingGate : null };
548
- if (id === OFFICIAL_SUBAGENT_EXECUTION_STAGE_ID)
549
- return { ...officialSubagentPipelineStage(officialSubagentPolicy), status: 'required', reason: officialSubagentPolicy.reason, ...metadata };
550
- if (id === 'engineering_sanity_check') {
551
- return {
552
- id,
553
- status: 'keep',
554
- reason: 'required_for_code_and_database_quality',
555
- checks: [
556
- 'trace actual callers and preserve basic SOLID responsibility and dependency boundaries',
557
- 'inspect loops, collections, resolvers, and serializers for N+1 or repeated I/O',
558
- 'prove render, recursion, event, retry, and polling loops are bounded and cancellable',
559
- 'reject disabled checks, swallowed errors, placeholder success, and verification bypasses',
560
- 'for DB work, verify the existing canonical adapter and connection/pool lifecycle before changes',
561
- 'for sensitive or multi-step DB work, verify transaction rollback, error propagation, idempotency, and post-commit invariants'
562
- ],
563
- ...metadata
564
- };
565
- }
566
- if (id === 'ambiguity_gate' && ambiguity?.required === false)
567
- return { id, status: 'not_applicable', reason: 'ambiguity_gate_not_required_for_entrypoint', ...metadata };
568
- if (id === 'ambiguity_gate' && ambiguity?.passed)
569
- return { id, status: 'passed', reason: 'ambiguity_contract_already_sealed', ...metadata };
570
- return { id, status: 'keep', reason: lane.fast_lane_allowed ? 'task_profile_minimal_lane' : 'required_by_task_and_route_profile', ...metadata };
571
- });
572
- }
573
- function planVerification(route, task, proof, budget) {
574
- if (budget === 'none')
575
- return [];
576
- const out = new Set(proof.verification || []);
577
- if (budget === 'single-check')
578
- out.add('run one focused check for the changed surface');
579
- if (budget === 'affected')
580
- out.add('run affected tests or checks for changed files');
581
- if (budget === 'confidence') {
582
- out.add('run the focused build or typecheck for the affected package');
583
- out.add('run affected regression tests and the risk-specific safety check');
584
- }
585
- if (budget === 'release') {
586
- out.add('npm run packcheck');
587
- out.add('sks selftest --mock --json');
588
- }
589
- if (route?.id === 'Naruto')
590
- out.add('validate official subagent evidence and the parent integration summary');
591
- if (routeNeedsEngineeringSanityReview(route, task)) {
592
- out.add('complete the engineering sanity review for SOLID boundaries, N+1/repeated I/O, bounded loops, and verification bypasses');
593
- }
594
- if (route?.id === 'DB' || route?.id === 'MadSKS') {
595
- out.add('verify existing canonical DB access, pool lifecycle, and transaction integrity for sensitive or multi-step mutations');
596
- }
597
- if (reflectionRequiredForRoute(route))
598
- out.add('sks wiki validate .sneakoscope/wiki/context-pack.json');
599
- return [...out];
600
- }
601
- function pipelineInvariants(input) {
602
- const out = ['no_unrequested_fallback_code'];
603
- if (input.stages.some((stage) => stage.id === 'engineering_sanity_check'))
604
- out.push('engineering_sanity_check');
605
- if (input.verificationBudget !== 'none')
606
- out.push('listed_verification');
607
- if (input.stages.some((stage) => stage.id === 'ssot_guard' && stage.status !== 'not_applicable'))
608
- out.push('ssot_guard');
609
- if (input.stages.some((stage) => stage.id === 'triwiki_validate_before_final'))
610
- out.push('triwiki_validate_before_final');
611
- if (input.gateProfile !== 'none')
612
- out.push('honest_summary');
613
- return out;
614
- }
615
469
  function planNextActions(route, task, taskProfile, ambiguity, lane) {
616
470
  if (taskProfile === 'passthrough' || taskProfile === 'answer')
617
471
  return [];
@@ -1593,15 +1447,7 @@ function strictNarutoIntegerOption(text, option) {
1593
1447
  function routeState(id, route, phase, context7Required, extra = {}) {
1594
1448
  const reasoning = routeReasoning(route, extra.prompt || '');
1595
1449
  const subagentsRequired = routeRequiresSubagents(route, extra.prompt || '');
1596
- return { mission_id: id, route: route.id, route_command: route.command, mode: route.mode, phase, context7_required: context7Required, context7_verified: false, subagents_required: subagentsRequired, subagents_verified: !subagentsRequired, native_sessions_required: false, native_sessions_verified: false, reflection_required: reflectionRequiredForRoute(route), engineering_sanity_required: false, engineering_sanity_scope_base: null, visible_progress_required: true, context_tracking: 'triwiki', required_skills: route.requiredSkills, stop_gate: route.stopGate, reasoning_effort: reasoning.effort, reasoning_profile: reasoning.profile, reasoning_temporary: true, goal_continuation: ambientGoalContinuation(), ...extra };
1597
- }
1598
- function routeNeedsEngineeringSanityReview(route, task) {
1599
- const id = String(route?.id || '');
1600
- if (['DB', 'MadSKS'].includes(id))
1601
- return true;
1602
- if (['Answer', 'Help', 'Wiki', 'Goal', 'Research', 'AutoResearch', 'PPT', 'ImageUXReview', 'ComputerUse', 'GX'].includes(id))
1603
- return false;
1604
- return looksLikeCodeChangingWork(String(task || ''));
1450
+ return { mission_id: id, route: route.id, route_command: route.command, mode: route.mode, phase, context7_required: context7Required, context7_verified: false, subagents_required: subagentsRequired, subagents_verified: !subagentsRequired, native_sessions_required: false, native_sessions_verified: false, reflection_required: reflectionRequiredForRoute(route), engineering_sanity_required: false, engineering_sanity_scope_base: null, architecture_map_required: false, visible_progress_required: true, context_tracking: 'triwiki', required_skills: route.requiredSkills, stop_gate: route.stopGate, reasoning_effort: reasoning.effort, reasoning_profile: reasoning.profile, reasoning_temporary: true, goal_continuation: ambientGoalContinuation(), ...extra };
1605
1451
  }
1606
1452
  function routeContext(route, id, task, required, next) {
1607
1453
  const visibleTask = stripVisibleDecisionAnswerBlocks(task);
@@ -17,6 +17,8 @@ import { checkStopGate } from '../stop-gate/stop-gate-check.js';
17
17
  import { readWorkOrderLedger, evaluateWorkOrderCoverage } from '../work-order-ledger.js';
18
18
  import { DB_ACCESS_REVIEW_ARTIFACT, DB_REVIEW_ARTIFACT, validateDbAccessReviewArtifact, validateDbReview, validateMadSksSqlPlaneCompletion } from '../db-review.js';
19
19
  import { ENGINEERING_SANITY_REVIEW_ARTIFACT, validateEngineeringSanityReviewArtifact } from '../engineering-sanity-review.js';
20
+ import { ARCHITECTURE_MAP_REVIEW_ARTIFACT } from '../architecture-map-review.js';
21
+ import { architectureMapGateStatus } from '../architecture-map-pipeline.js';
20
22
  import { clarificationStopReason, context7Evidence, hasContext7DocsEvidence, hasSubagentEvidence, subagentEvidence, PIPELINE_PLAN_ARTIFACT, } from './runtime-core.js';
21
23
  import { projectTriwikiToAgentsMd } from '../triwiki/agents-md-projector.js';
22
24
  import { effectiveSubagentTarget, normalizeLegacySubagentCountFields, subagentCountContractBlockers } from '../subagents/wave-lifecycle.js';
@@ -256,6 +258,7 @@ export async function evaluateStop(root, state, payload, opts = {}) {
256
258
  && !completionProofRequired
257
259
  && !reflectionRequired
258
260
  && state?.engineering_sanity_required !== true
261
+ && state?.architecture_map_required !== true
259
262
  && state?.db_access_review_required !== true) {
260
263
  return null;
261
264
  }
@@ -267,12 +270,14 @@ export async function evaluateStop(root, state, payload, opts = {}) {
267
270
  && activeGatePreview?.covered_gate === 'official-subagent-evidence';
268
271
  const dbAccessPromise = dbAccessReviewGateStatus(root, state, jsonCache);
269
272
  const engineeringSanityPromise = engineeringSanityGateStatus(root, state, jsonCache);
273
+ const architectureMapPromise = architectureMapGateStatus(root, state);
270
274
  const context7Promise = state?.context7_required ? hasContext7DocsEvidence(root, state) : Promise.resolve(true);
271
275
  const subagentPromise = state?.subagents_required && !activeGateNotApplicable ? hasSubagentEvidence(root, state) : Promise.resolve(true);
272
276
  const mistakeRecallPromise = mistakeRecallGateStatus(root, state);
273
- const [dbAccess, engineeringSanity, context7Ok, subagentOk, mistakeRecall] = await Promise.all([
277
+ const [dbAccess, engineeringSanity, architectureMap, context7Ok, subagentOk, mistakeRecall] = await Promise.all([
274
278
  dbAccessPromise,
275
279
  engineeringSanityPromise,
280
+ architectureMapPromise,
276
281
  context7Promise,
277
282
  subagentPromise,
278
283
  mistakeRecallPromise
@@ -280,6 +285,7 @@ export async function evaluateStop(root, state, payload, opts = {}) {
280
285
  await resolveRecoveredComplianceBlocker(root, state, {
281
286
  [DB_ACCESS_REVIEW_ARTIFACT]: dbAccess.ok,
282
287
  [ENGINEERING_SANITY_REVIEW_ARTIFACT]: engineeringSanity.ok,
288
+ [ARCHITECTURE_MAP_REVIEW_ARTIFACT]: architectureMap.ok,
283
289
  'context7-evidence': context7Ok,
284
290
  'official-subagent-evidence': subagentOk
285
291
  });
@@ -289,6 +295,9 @@ export async function evaluateStop(root, state, payload, opts = {}) {
289
295
  if (!engineeringSanity.ok) {
290
296
  return complianceBlock(root, state, `SKS ${state.route_command || state.mode || 'route'} must complete the engineering sanity review before completion. Static candidates are advisory only; inspect added hunks and real callers for SOLID boundaries, N+1/repeated I/O, bounded render/recursion/event/retry/polling behavior, and verification bypasses. Missing: ${(engineeringSanity.blockers || []).join(', ')}.`, { gate: ENGINEERING_SANITY_REVIEW_ARTIFACT, missing: engineeringSanity.blockers });
291
297
  }
298
+ if (!architectureMap.ok) {
299
+ return complianceBlock(root, state, `SKS ${state.route_command || state.mode || 'route'} must seal Architecture Map baseline before mutation and produce a passing architecture-map-review before completion. Missing: ${(architectureMap.blockers || []).join(', ')}.`, { gate: ARCHITECTURE_MAP_REVIEW_ARTIFACT, missing: architectureMap.blockers });
300
+ }
292
301
  if (state?.context7_required && !context7Ok) {
293
302
  return complianceBlock(root, state, `SKS ${state.route_command || state.mode || 'route'} requires Context7 evidence before completion. Use Context7 resolve-library-id, then query-docs, so SKS can record context7-evidence.jsonl.`, { gate: 'context7-evidence' });
294
303
  }
@@ -518,6 +527,8 @@ async function markReflectionInvalidatedForGateFailure(root, state = {}, detail
518
527
  return;
519
528
  if (/^work-order-ledger$/i.test(gate) || /^completion-proof$/i.test(gate))
520
529
  return;
530
+ if (/^atlas:/.test(gate) || /^architecture-map:/.test(gate) || /(^|[\\/])?(architecture-atlas-review|architecture-map-review|atlas-gate)(\.json)?$/i.test(gate))
531
+ return;
521
532
  await setCurrent(root, {
522
533
  mission_id: state.mission_id,
523
534
  reflection_invalidation_required: true,
@@ -0,0 +1,70 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ export const CONTENT_DIGEST_CACHE_SCHEMA = 'sks.content-digest-cache.v1';
5
+ export const CONTENT_DIGEST_CACHE_ENV = 'SKS_RELEASE_CONTENT_DIGEST_CACHE';
6
+ const processMemo = new Map();
7
+ export function contentDigestCacheEnabled() {
8
+ return process.env[CONTENT_DIGEST_CACHE_ENV] !== '0';
9
+ }
10
+ export function contentDigestCachePath(root) {
11
+ return path.join(root, '.sneakoscope', 'cache', 'release-digests.json');
12
+ }
13
+ export function statFingerprint(root, files) {
14
+ const hash = crypto.createHash('sha256');
15
+ for (const file of files) {
16
+ let stat;
17
+ try {
18
+ stat = fs.statSync(path.isAbsolute(file) ? file : path.join(root, file), { bigint: true });
19
+ }
20
+ catch {
21
+ hash.update(`${file}\0missing\0`);
22
+ continue;
23
+ }
24
+ hash.update(`${file}\0${stat.size}\0${stat.mtimeNs}\0${stat.ino}\0`);
25
+ }
26
+ return hash.digest('hex');
27
+ }
28
+ export function cachedByFingerprint(root, bucket, fingerprint, compute) {
29
+ if (!contentDigestCacheEnabled())
30
+ return compute();
31
+ const memoKey = `${path.resolve(root)}\0${bucket}`;
32
+ const memo = processMemo.get(memoKey);
33
+ if (memo?.fingerprint === fingerprint)
34
+ return structuredClone(memo.value);
35
+ const document = readCache(root);
36
+ const stored = document.entries[bucket];
37
+ if (stored && stored.fingerprint === fingerprint) {
38
+ processMemo.set(memoKey, { fingerprint, value: stored.value });
39
+ return structuredClone(stored.value);
40
+ }
41
+ const value = compute();
42
+ processMemo.set(memoKey, { fingerprint, value });
43
+ writeCache(root, bucket, fingerprint, value);
44
+ return value;
45
+ }
46
+ export function clearContentDigestProcessMemo() {
47
+ processMemo.clear();
48
+ }
49
+ function readCache(root) {
50
+ try {
51
+ const parsed = JSON.parse(fs.readFileSync(contentDigestCachePath(root), 'utf8'));
52
+ if (parsed?.schema === CONTENT_DIGEST_CACHE_SCHEMA && parsed.entries && typeof parsed.entries === 'object')
53
+ return parsed;
54
+ }
55
+ catch { }
56
+ return { schema: CONTENT_DIGEST_CACHE_SCHEMA, entries: {} };
57
+ }
58
+ function writeCache(root, bucket, fingerprint, value) {
59
+ const file = contentDigestCachePath(root);
60
+ try {
61
+ const document = readCache(root);
62
+ document.entries[bucket] = { fingerprint, value, recorded_at: new Date().toISOString() };
63
+ fs.mkdirSync(path.dirname(file), { recursive: true });
64
+ const temp = `${file}.${process.pid}.tmp`;
65
+ fs.writeFileSync(temp, `${JSON.stringify(document, null, 2)}\n`);
66
+ fs.renameSync(temp, file);
67
+ }
68
+ catch {
69
+ }
70
+ }
@@ -65,6 +65,15 @@ function costFor(id) {
65
65
  export function affectedGlobsFor(id) {
66
66
  const prefix = id.split(':')[0];
67
67
  switch (prefix) {
68
+ case 'architecture-map':
69
+ return [
70
+ 'src/core/triwiki/context-graph/architecture/**',
71
+ 'src/core/triwiki/context-graph/projections/mermaid/**',
72
+ 'src/core/triwiki/context-graph/store/architecture-map-store.ts',
73
+ 'src/scripts/architecture-map-check.ts',
74
+ 'config/architecture-map-policy.v1.json',
75
+ 'package.json'
76
+ ];
68
77
  case 'architecture':
69
78
  return ['src/core/safety/ssot-guard.ts', 'src/core/pipeline-internals/runtime-core.ts', 'src/core/pipeline-internals/runtime-gates.ts', 'src/core/commands/naruto-command.ts', 'src/scripts/release-parallel-check.ts', 'src/scripts/architecture-guard-check.ts', 'docs/architecture-ts-rust-boundary.md', 'package.json'];
70
79
  case 'core-skill':
@@ -1,11 +1,12 @@
1
1
  import path from 'node:path';
2
+ import { sameFilesystemPathSync } from '../fsx.js';
2
3
  import { readMacosMenubarProof, validateMacosMenubarProofArtifacts } from './macos-menubar-proof.js';
3
4
  import { fileSha256, gitOk, gitText, readJson, relative, unique } from './release-proof-io.js';
4
5
  import { releaseOriginIdentity } from './release-origin.js';
5
6
  import { releaseProofDir, validateLocalReleasePackBinding } from './release-pack-receipt.js';
6
7
  import { validatePhysicalReleaseGateInspection } from './physical-release-gates.js';
7
8
  import { validateFullReleaseStamp } from './release-stamp-proof.js';
8
- import { RELEASE_UPGRADE_BASELINE_SHA256, RELEASE_UPGRADE_BASELINE_VERSION, releaseUpgradeExpectedVersion, releaseUpgradeProofFilename } from './release-upgrade-baseline.js';
9
+ import { RELEASE_UPGRADE_BASELINE_SHA256, RELEASE_UPGRADE_BASELINE_VERSION, isExpectedReleaseUpgradeLaunchctlCall, releaseUpgradeExpectedVersion, releaseUpgradeProofFilename } from './release-upgrade-baseline.js';
9
10
  export const MAIN_PUSH_GUARD_SCHEMA = 'sks.release-main-push-guard.v1';
10
11
  const RELEASE_UPGRADE_SMOKE_SCHEMA = 'sks.release-upgrade-smoke.v2';
11
12
  const RELEASE_UPGRADE_COMMAND_STAGES = Object.freeze([
@@ -244,7 +245,7 @@ function validateReleaseUpgradeIsolationAndCommands(report, blockers) {
244
245
  || !path.isAbsolute(String(safety?.launchctl_log_path || ''))
245
246
  || !lexicalSubpath(String(safety?.launchctl_log_path || ''), sandbox)
246
247
  || !Array.isArray(safety?.launchctl_calls)
247
- || safety.launchctl_calls.some((call) => !/^(?:unsetenv (?:CODEX_LB_API_KEY|OPENROUTER_API_KEY)|print)$/.test(String(call)))
248
+ || safety.launchctl_calls.some((call) => !isExpectedReleaseUpgradeLaunchctlCall(call))
248
249
  || !Array.isArray(safety?.launchctl_unexpected_calls)
249
250
  || safety.launchctl_unexpected_calls.length > 0)
250
251
  blockers.push('install_safety_policy_invalid');
@@ -367,7 +368,7 @@ function validateDoctorCommandReceipt(command, stage, reportFile, isolation, tar
367
368
  || binding.matches_stdout !== true
368
369
  || binding.schema !== 'sks.doctor-status.v3'
369
370
  || binding.ok !== true
370
- || binding.root !== isolation.workspace)
371
+ || !sameFilesystemPathSync(binding.root, isolation.workspace))
371
372
  blockers.push(`doctor_report_binding_invalid:${stage}`);
372
373
  }
373
374
  else if (stage !== 'target_doctor' && binding.validation_mode === 'pinned_baseline_stdout_only') {
@@ -1,6 +1,7 @@
1
1
  import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { cachedByFingerprint, statFingerprint } from './content-digest-cache.js';
4
5
  export function packageDistSnapshot(root, pkg) {
5
6
  const distRoot = path.join(root, 'dist');
6
7
  if (!fs.existsSync(distRoot))
@@ -11,6 +12,9 @@ export function packageDistSnapshot(root, pkg) {
11
12
  const included = files
12
13
  .filter((file) => packageFileIncluded(`dist/${path.relative(distRoot, file).split(path.sep).join('/')}`, entries))
13
14
  .sort();
15
+ return cachedByFingerprint(root, 'package-dist-snapshot', statFingerprint(root, included), () => computePackageDistSnapshot(distRoot, included));
16
+ }
17
+ function computePackageDistSnapshot(distRoot, included) {
14
18
  const hash = crypto.createHash('sha256');
15
19
  for (const file of included) {
16
20
  const rel = path.relative(distRoot, file).split(path.sep).join('/');
@@ -48,6 +52,10 @@ export function packageFilesSnapshot(root, pkg) {
48
52
  }
49
53
  }
50
54
  const files = [...candidates].filter((file) => packageFileIncluded(file, entries)).sort();
55
+ const fingerprint = statFingerprint(root, [...missingEntries.map((entry) => `missing:${entry}`), ...files]);
56
+ return cachedByFingerprint(root, 'package-files-snapshot', fingerprint, () => computePackageFilesSnapshot(root, files, missingEntries));
57
+ }
58
+ function computePackageFilesSnapshot(root, files, missingEntries) {
51
59
  const hash = crypto.createHash('sha256');
52
60
  for (const entry of missingEntries.sort()) {
53
61
  hash.update(entry);
@@ -95,6 +103,15 @@ function packageFileIncluded(file, entries) {
95
103
  }
96
104
  return included;
97
105
  }
106
+ const PATTERN_REGEXP_CACHE = new Map();
107
+ function packagePatternRegExps(normalized) {
108
+ const cached = PATTERN_REGEXP_CACHE.get(normalized);
109
+ if (cached)
110
+ return cached;
111
+ const compiled = expandBracePatterns(normalized).map((expanded) => globPatternToRegExp(expanded));
112
+ PATTERN_REGEXP_CACHE.set(normalized, compiled);
113
+ return compiled;
114
+ }
98
115
  function matchesPackagePattern(file, pattern) {
99
116
  const rel = normalizeRel(file);
100
117
  const normalized = normalizeRel(pattern);
@@ -102,8 +119,7 @@ function matchesPackagePattern(file, pattern) {
102
119
  return false;
103
120
  if (!hasGlob(normalized))
104
121
  return rel === normalized || rel.startsWith(`${normalized}/`);
105
- for (const expanded of expandBracePatterns(normalized)) {
106
- const re = globPatternToRegExp(expanded);
122
+ for (const re of packagePatternRegExps(normalized)) {
107
123
  if (re.test(rel))
108
124
  return true;
109
125
  const parts = rel.split('/');
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { desktopBridgeReleaseSourceIdentitySha256, inspectDesktopBridgeReleaseEvidence } from './desktop-bridge-release-evidence.js';
6
+ import { cloneBuffer, fileIdentity, inspectionKey, memoizeReleaseInspection, toolIdentity } from './release-inspection-memo.js';
6
7
  export const PHYSICAL_RELEASE_GATES_SCHEMA = 'sks.release-physical-gates.v2';
7
8
  export const PHYSICAL_RELEASE_GATES_INSPECTION_SCHEMA = 'sks.release-physical-gates-inspection.v2';
8
9
  export const PHYSICAL_RELEASE_EVIDENCE_ARTIFACT_MANIFEST_SCHEMA = 'sks.release-physical-evidence-artifact-manifest.v1';
@@ -375,6 +376,16 @@ function inspectPhysicalEvidenceArtifact(input) {
375
376
  };
376
377
  }
377
378
  function verifyGithubArtifactAttestation(input) {
379
+ const key = inspectionKey(input.archivePath, fileIdentity(input.archivePath), toolIdentity('gh'), input.repository, input.trustedWorkflow, input.sourceCommit, input.runId);
380
+ const verdict = memoizeReleaseInspection('github-artifact-attestation', key, () => {
381
+ const blockers = [];
382
+ const invocationUri = computeGithubArtifactAttestation({ ...input, blockers });
383
+ return { invocation_uri: invocationUri, blockers };
384
+ });
385
+ input.blockers.push(...verdict.blockers);
386
+ return verdict.invocation_uri;
387
+ }
388
+ function computeGithubArtifactAttestation(input) {
378
389
  const result = spawnSync('gh', [
379
390
  'attestation', 'verify', input.archivePath,
380
391
  '--repo', input.repository,
@@ -429,6 +440,15 @@ function readBoundArchive(file) {
429
440
  }
430
441
  }
431
442
  function listArchiveFiles(archivePath, blockers) {
443
+ const listing = memoizeReleaseInspection('physical-archive-listing', inspectionKey(archivePath, fileIdentity(archivePath), toolIdentity('tar')), () => {
444
+ const own = [];
445
+ const files = computeArchiveFiles(archivePath, own);
446
+ return { entries: files ? [...files] : null, blockers: own };
447
+ });
448
+ blockers.push(...listing.blockers);
449
+ return listing.entries ? new Map(listing.entries) : null;
450
+ }
451
+ function computeArchiveFiles(archivePath, blockers) {
432
452
  const result = spawnSync('tar', ['-tzf', archivePath], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
433
453
  if (result.status !== 0) {
434
454
  blockers.push('physical_evidence_artifact_archive_unreadable');
@@ -455,12 +475,15 @@ function listArchiveFiles(archivePath, blockers) {
455
475
  return files;
456
476
  }
457
477
  function readArchiveMember(archivePath, member, label, blockers) {
458
- const result = spawnSync('tar', ['-xOzf', archivePath, member], { encoding: 'buffer', maxBuffer: 64 * 1024 * 1024 });
459
- if (result.status !== 0 || !Buffer.isBuffer(result.stdout)) {
478
+ const bytes = memoizeReleaseInspection('physical-archive-member', inspectionKey(archivePath, fileIdentity(archivePath), toolIdentity('tar'), member), () => {
479
+ const result = spawnSync('tar', ['-xOzf', archivePath, member], { encoding: 'buffer', maxBuffer: 64 * 1024 * 1024 });
480
+ return result.status !== 0 || !Buffer.isBuffer(result.stdout) ? null : result.stdout;
481
+ }, (value) => (value ? cloneBuffer(value) : null));
482
+ if (!bytes) {
460
483
  blockers.push(`${label}_unreadable`);
461
484
  return null;
462
485
  }
463
- return result.stdout;
486
+ return bytes;
464
487
  }
465
488
  function safeArchivePath(value) {
466
489
  if (!value || value.includes('\\') || value.startsWith('/') || value.endsWith('/'))
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process';
2
2
  import crypto from 'node:crypto';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
+ import { cachedByFingerprint, statFingerprint } from './content-digest-cache.js';
5
6
  import { packageDistSnapshot, packageFilesSnapshot } from './package-dist-snapshot.js';
6
7
  export const RELEASE_AUTHORIZATION_SNAPSHOT_KEYS = Object.freeze([
7
8
  'git_commit',
@@ -44,6 +45,9 @@ function releaseGateHash(root, pkg) {
44
45
  }
45
46
  function releaseSourceSnapshot(root) {
46
47
  const files = gitFiles(root).filter(releaseRelevant).sort();
48
+ return cachedByFingerprint(root, 'release-source-snapshot', statFingerprint(root, files), () => computeReleaseSourceSnapshot(root, files));
49
+ }
50
+ function computeReleaseSourceSnapshot(root, files) {
47
51
  const hash = crypto.createHash('sha256');
48
52
  for (const file of files) {
49
53
  const full = path.join(root, file);
@@ -292,10 +292,14 @@ function writeReleaseCacheBridgeReport(root, row) {
292
292
  parsed = existing;
293
293
  }
294
294
  catch { }
295
+ const stamped = { ...row, recorded_at: new Date().toISOString() };
295
296
  parsed.records = [
296
- ...parsed.records.filter((item) => item.gate_id !== row.gate_id || item.cache_key !== row.cache_key || item.bridge !== row.bridge),
297
- row
298
- ].sort((a, b) => `${a.gate_id}:${a.bridge}`.localeCompare(`${b.gate_id}:${b.bridge}`));
297
+ ...parsed.records.filter((item) => item.gate_id !== row.gate_id || item.bridge !== row.bridge),
298
+ stamped
299
+ ]
300
+ .sort((a, b) => Date.parse(String(b.recorded_at || 0)) - Date.parse(String(a.recorded_at || 0)) || 0)
301
+ .slice(0, releaseCacheBridgeRecordLimit())
302
+ .sort((a, b) => `${a.gate_id}:${a.bridge}`.localeCompare(`${b.gate_id}:${b.bridge}`));
299
303
  fs.mkdirSync(path.dirname(file), { recursive: true });
300
304
  fs.writeFileSync(file, `${JSON.stringify(parsed, null, 2)}\n`);
301
305
  }
@@ -317,9 +321,29 @@ function writeCacheRecord(file, key, record) {
317
321
  parsed.schema = RELEASE_GATE_CACHE_V2_SCHEMA;
318
322
  parsed.records ||= {};
319
323
  parsed.records[key] = record;
324
+ parsed.records = pruneCacheRecords(parsed.records);
320
325
  fs.mkdirSync(path.dirname(file), { recursive: true });
321
326
  fs.writeFileSync(file, `${JSON.stringify(parsed, null, 2)}\n`);
322
327
  }
328
+ export function pruneCacheRecords(records, keep = releaseGateCacheRecordLimit()) {
329
+ const entries = Object.entries(records || {});
330
+ if (entries.length <= keep)
331
+ return records;
332
+ const ordered = entries.sort((left, right) => recordedAtMs(right[1]) - recordedAtMs(left[1]));
333
+ return Object.fromEntries(ordered.slice(0, keep));
334
+ }
335
+ export function releaseGateCacheRecordLimit() {
336
+ const configured = Number(process.env.SKS_RELEASE_GATE_CACHE_RECORD_LIMIT);
337
+ return Number.isFinite(configured) && configured > 0 ? Math.floor(configured) : 600;
338
+ }
339
+ export function releaseCacheBridgeRecordLimit() {
340
+ const configured = Number(process.env.SKS_RELEASE_CACHE_BRIDGE_RECORD_LIMIT);
341
+ return Number.isFinite(configured) && configured > 0 ? Math.floor(configured) : 600;
342
+ }
343
+ function recordedAtMs(record) {
344
+ const parsed = Date.parse(String(record?.recorded_at || ''));
345
+ return Number.isFinite(parsed) ? parsed : 0;
346
+ }
323
347
  function hashFileIfPresent(hash, file) {
324
348
  if (fs.existsSync(file) && fs.statSync(file).isFile())
325
349
  hash.update(fs.readFileSync(file));
@@ -2,6 +2,8 @@ import crypto from 'node:crypto';
2
2
  export const RELEASE_GATE_CONTRACT_SCHEMA = 'sks.release-gate-contract.v1';
3
3
  export const RELEASE_GATE_CONTRACT_IDS = Object.freeze([
4
4
  'architecture:guard',
5
+ 'architecture-map:contract',
6
+ 'architecture-map:legacy-closure',
5
7
  'codex:current:app-server-v2',
6
8
  'codex:current:binary-identity',
7
9
  'codex:current:capability',
@@ -10,6 +10,7 @@ import { RELEASE_GATE_NODE_SCHEMA, validateReleaseGateManifest } from './release
10
10
  import { countReleaseGateResources, defaultReleaseGateBudget, summarizeReleaseGateBudget } from './release-gate-resource-governor.js';
11
11
  import { selectAffectedReleaseGates } from './release-gate-affected-selector.js';
12
12
  import { computeTriWikiAffectedGraph } from '../triwiki/triwiki-affected-graph.js';
13
+ import { pruneTriWikiProofBank } from '../triwiki/triwiki-proof-bank.js';
13
14
  import { executeExtremeSchedule } from './extreme-parallel-scheduler.js';
14
15
  import { computeResourceClassBudget } from './resource-class-budget.js';
15
16
  import { guardedProcessKill, guardContextForRoute } from '../safety/mutation-guard.js';
@@ -106,6 +107,7 @@ export async function runReleaseGateDag(input) {
106
107
  throw new Error(`release_gate_selection_missing_dependencies:${[...new Set(missingSelectedDeps)].sort().join(',')}`);
107
108
  const runId = `rg-${new Date().toISOString().replace(/[:.]/g, '-')}-${process.pid}`;
108
109
  const retentionBefore = await pruneOldReleaseGateRunDirs(root);
110
+ const proofBankRetention = pruneTriWikiProofBank(root);
109
111
  await sweepSksTempDirs(root, { maxAgeHours: 24 }).catch(() => null);
110
112
  const reportDir = path.join(root, '.sneakoscope', 'reports', 'release-gates', runId);
111
113
  fs.mkdirSync(reportDir, { recursive: true });
@@ -113,6 +115,7 @@ export async function runReleaseGateDag(input) {
113
115
  const affectedGraphFile = path.join(reportDir, 'affected-gate-graph.json');
114
116
  const completionCertificateFile = path.join(reportDir, 'completion-certificate.json');
115
117
  appendReleaseGateJsonl(timeline, { event: 'retention', phase: 'before_run', ...retentionBefore, at: new Date().toISOString() });
118
+ appendReleaseGateJsonl(timeline, { event: 'proof_bank_retention', phase: 'before_run', ...proofBankRetention, at: new Date().toISOString() });
116
119
  const started = Date.now();
117
120
  const slaMs = Math.max(1, Math.floor(Number(input.slaMs || 300000)));
118
121
  const pending = new Map(selected.map((gate) => [gate.id, gate]));