sneakoscope 4.2.0 → 4.3.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 (126) hide show
  1. package/README.md +35 -8
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/bin/sks.js +1 -1
  6. package/dist/cli/command-registry.js +3 -1
  7. package/dist/cli/ultra-search-command.js +163 -0
  8. package/dist/cli/xai-command.js +28 -168
  9. package/dist/core/agents/agent-codex-cockpit.js +3 -3
  10. package/dist/core/agents/agent-runner-ollama.js +2 -0
  11. package/dist/core/agents/agent-wrongness.js +1 -1
  12. package/dist/core/agents/native-worker-backend-router.js +3 -0
  13. package/dist/core/bench.js +115 -0
  14. package/dist/core/code-structure.js +399 -11
  15. package/dist/core/codex-control/codex-app-server-v2-client.js +86 -2
  16. package/dist/core/codex-control/codex-fake-sdk-adapter.js +67 -9
  17. package/dist/core/codex-control/codex-reliability-shield.js +26 -5
  18. package/dist/core/codex-control/codex-task-runner.js +7 -1
  19. package/dist/core/codex-control/gpt-final-arbiter.js +4 -1
  20. package/dist/core/codex-control/gpt-final-review-schema.js +58 -0
  21. package/dist/core/codex-control/model-call-concurrency.js +1 -1
  22. package/dist/core/codex-native/core-skill-manifest.js +23 -0
  23. package/dist/core/commands/bench-command.js +11 -2
  24. package/dist/core/commands/code-structure-command.js +34 -2
  25. package/dist/core/commands/qa-loop-command.js +23 -7
  26. package/dist/core/commands/run-command.js +92 -2
  27. package/dist/core/commands/seo-command.js +130 -0
  28. package/dist/core/feature-fixtures.js +6 -0
  29. package/dist/core/feature-registry.js +3 -1
  30. package/dist/core/fsx.js +1 -1
  31. package/dist/core/hooks-runtime.js +9 -1
  32. package/dist/core/init.js +8 -6
  33. package/dist/core/lean-engineering-policy.js +159 -0
  34. package/dist/core/pipeline-internals/runtime-core.js +15 -5
  35. package/dist/core/proof/auto-finalize.js +3 -2
  36. package/dist/core/proof/proof-schema.js +2 -1
  37. package/dist/core/proof/proof-writer.js +1 -0
  38. package/dist/core/proof/route-adapter.js +4 -2
  39. package/dist/core/proof/route-finalizer.js +35 -3
  40. package/dist/core/qa-loop/qa-app-server-driver.js +134 -0
  41. package/dist/core/qa-loop/qa-contract-v2.js +231 -0
  42. package/dist/core/qa-loop/qa-gate-v2.js +132 -0
  43. package/dist/core/qa-loop/qa-runtime-artifacts.js +53 -0
  44. package/dist/core/qa-loop/qa-surface-router.js +114 -0
  45. package/dist/core/qa-loop/qa-types.js +18 -0
  46. package/dist/core/qa-loop.js +83 -26
  47. package/dist/core/release/gate-manifest.js +1 -0
  48. package/dist/core/release/sla-scheduler.js +1 -1
  49. package/dist/core/release-parallel-full-coverage.js +1 -1
  50. package/dist/core/routes.js +96 -14
  51. package/dist/core/search-visibility/adapter-registry.js +26 -0
  52. package/dist/core/search-visibility/adapters/next-app.js +6 -0
  53. package/dist/core/search-visibility/adapters/next-pages.js +6 -0
  54. package/dist/core/search-visibility/adapters/static-site.js +6 -0
  55. package/dist/core/search-visibility/analyzers.js +377 -0
  56. package/dist/core/search-visibility/artifacts.js +183 -0
  57. package/dist/core/search-visibility/discovery.js +347 -0
  58. package/dist/core/search-visibility/index.js +199 -0
  59. package/dist/core/search-visibility/mission.js +67 -0
  60. package/dist/core/search-visibility/mutation.js +314 -0
  61. package/dist/core/search-visibility/types.js +2 -0
  62. package/dist/core/search-visibility/verifier.js +60 -0
  63. package/dist/core/source-intelligence/source-intelligence-policy.js +45 -26
  64. package/dist/core/source-intelligence/source-intelligence-proof.js +10 -16
  65. package/dist/core/source-intelligence/source-intelligence-runner.js +56 -42
  66. package/dist/core/triwiki/triwiki-affected-graph.js +3 -2
  67. package/dist/core/trust-kernel/trust-report.js +3 -5
  68. package/dist/core/ultra-search/index.js +3 -0
  69. package/dist/core/ultra-search/runtime.js +502 -0
  70. package/dist/core/ultra-search/types.js +3 -0
  71. package/dist/core/version.js +1 -1
  72. package/dist/scripts/agent-visual-consistency-check.js +1 -1
  73. package/dist/scripts/check-architecture.js +40 -7
  74. package/dist/scripts/check-command-module-budget.js +43 -5
  75. package/dist/scripts/check-pipeline-budget.js +17 -30
  76. package/dist/scripts/check-publish-tag.js +33 -6
  77. package/dist/scripts/check-route-modularity.js +25 -33
  78. package/dist/scripts/check-runtime-schemas.js +22 -0
  79. package/dist/scripts/codex-control-all-pipelines-check.js +1 -0
  80. package/dist/scripts/codex-control-model-capacity-fallback-check.js +53 -0
  81. package/dist/scripts/config-managed-merge-callsite-coverage-check.js +7 -1
  82. package/dist/scripts/core-skill-immutable-sync-check.js +3 -2
  83. package/dist/scripts/core-skill-integrity-blackbox.js +3 -2
  84. package/dist/scripts/core-skill-manifest-check.js +7 -2
  85. package/dist/scripts/geo-claim-evidence-check.js +18 -0
  86. package/dist/scripts/geo-cli-blackbox-check.js +18 -0
  87. package/dist/scripts/geo-crawler-policy-check.js +16 -0
  88. package/dist/scripts/geo-llms-txt-optional-check.js +19 -0
  89. package/dist/scripts/gpt-final-arbiter-check.js +4 -1
  90. package/dist/scripts/loop-directive-check-lib.js +78 -1
  91. package/dist/scripts/qa-loop-app-server-driver-check.js +74 -0
  92. package/dist/scripts/qa-loop-surface-router-check.js +49 -0
  93. package/dist/scripts/release-check-dynamic-execute.js +1 -1
  94. package/dist/scripts/release-metadata-1-19-check.js +2 -2
  95. package/dist/scripts/release-parallel-check.js +17 -2
  96. package/dist/scripts/release-parallel-full-coverage-check.js +1 -1
  97. package/dist/scripts/release-readiness-report.js +6 -6
  98. package/dist/scripts/release-registry-check.js +33 -14
  99. package/dist/scripts/runtime-ts-rust-boundary-check.js +1 -1
  100. package/dist/scripts/search-visibility-gate-lib.js +124 -0
  101. package/dist/scripts/seo-audit-fixture-check.js +16 -0
  102. package/dist/scripts/seo-canonical-locale-check.js +19 -0
  103. package/dist/scripts/seo-cli-blackbox-check.js +18 -0
  104. package/dist/scripts/seo-geo-feature-fixture-quality-check.js +18 -0
  105. package/dist/scripts/seo-geo-geo-disambiguation-check.js +12 -0
  106. package/dist/scripts/seo-geo-no-unsupported-ranking-claims-check.js +18 -0
  107. package/dist/scripts/seo-geo-route-identity-check.js +12 -0
  108. package/dist/scripts/seo-geo-skill-rich-content-check.js +22 -0
  109. package/dist/scripts/seo-mutation-rollback-check.js +23 -0
  110. package/dist/scripts/seo-no-mutation-by-default-check.js +17 -0
  111. package/dist/scripts/seo-structured-data-visible-content-check.js +19 -0
  112. package/dist/scripts/sks-1-18-gate-lib.js +2 -2
  113. package/dist/scripts/sks-3-1-5-directive-check-lib.js +10 -1
  114. package/dist/scripts/source-intelligence-all-modes-check.js +9 -19
  115. package/dist/scripts/source-intelligence-policy-check.js +6 -6
  116. package/dist/scripts/triwiki-affected-graph-check.js +2 -2
  117. package/dist/scripts/ultra-search-provider-interface-check.js +27 -0
  118. package/package.json +26 -5
  119. package/schemas/search-visibility/finding-ledger.schema.json +36 -0
  120. package/schemas/search-visibility/gate.schema.json +22 -0
  121. package/schemas/search-visibility/mutation-plan.schema.json +27 -0
  122. package/schemas/search-visibility/site-inventory.schema.json +21 -0
  123. package/schemas/search-visibility/verification-report.schema.json +23 -0
  124. package/dist/core/mcp/xai-mcp-detector.js +0 -157
  125. package/dist/core/mcp/xai-search-adapter.js +0 -100
  126. package/dist/scripts/xai-mcp-capability-check.js +0 -14
@@ -4,6 +4,9 @@ import path from 'node:path';
4
4
  import { performance } from 'node:perf_hooks';
5
5
  import { ensureDir, nowIso, packageRoot, projectRoot, runProcess, writeJsonAtomic, writeTextAtomic } from './fsx.js';
6
6
  import { percentile } from './perf-bench.js';
7
+ import { runFakeCodexSdkTask } from './codex-control/codex-fake-sdk-adapter.js';
8
+ import { GPT_FINAL_ARBITER_RESULT_SCHEMA_ID, gptFinalArbiterResultSchema } from './codex-control/gpt-final-review-schema.js';
9
+ import { LEAN_ENGINEERING_POLICY_HASH, LEAN_ENGINEERING_POLICY_ID, leanEngineeringCompactText } from './lean-engineering-policy.js';
7
10
  export const CORE_BENCH_BUDGET_TIERS = Object.freeze({
8
11
  'source-local': {
9
12
  'sks --version': 50,
@@ -76,6 +79,16 @@ export const UX_REVIEW_STAGED_LATENCY_BUDGETS = Object.freeze({
76
79
  codex_lb_status_probe_batch: 5_000,
77
80
  agent_status_probe_batch: 5_000
78
81
  });
82
+ export const LEAN_POLICY_BENCH_SCENARIOS = Object.freeze([
83
+ { id: 'date-input-overbuild', group: 'overbuild', candidate: 'same helper reimplementation for date input parsing' },
84
+ { id: 'color-input-overbuild', group: 'overbuild', candidate: 'one implementation factory for color input UI' },
85
+ { id: 'csv-export-dependency', group: 'overbuild', candidate: 'new dependency for simple CSV export despite stdlib support' },
86
+ { id: 'cache-fallback', group: 'overbuild', candidate: 'hidden mock fallback for cache miss success' },
87
+ { id: 'path-traversal-safety', group: 'safety', candidate: 'path traversal candidate without trust boundary validation' },
88
+ { id: 'sql-parameterization-safety', group: 'safety', candidate: 'sql injection candidate using string concatenation' },
89
+ { id: 'secret-redaction-safety', group: 'safety', candidate: 'secret leak candidate that removes redaction' },
90
+ { id: 'quota-limit-safety', group: 'safety', candidate: 'delete validation for quota limit one-liner' }
91
+ ]);
79
92
  const STATIC_CORE_COMMANDS = Object.freeze([
80
93
  ['sks --version', ['--version']],
81
94
  ['sks help', ['help']],
@@ -264,4 +277,106 @@ export async function writeCoreBenchArtifacts(root, report) {
264
277
  export async function benchRoot() {
265
278
  return projectRoot();
266
279
  }
280
+ export async function runLeanPolicyBench(root = process.cwd()) {
281
+ const rows = [];
282
+ for (const scenario of LEAN_POLICY_BENCH_SCENARIOS) {
283
+ const baseline = await fakeGptFinalScenario(String(scenario.candidate), false);
284
+ const lean = await fakeGptFinalScenario(String(scenario.candidate), true);
285
+ const expectedBaseline = scenario.group === 'safety' ? 'rejected' : 'approved';
286
+ const expectedLean = scenario.group === 'safety' ? 'rejected' : 'needs_more_work';
287
+ rows.push({
288
+ id: scenario.id,
289
+ group: scenario.group,
290
+ baseline_status: baseline.status,
291
+ lean_status: lean.status,
292
+ baseline_expected: expectedBaseline,
293
+ lean_expected: expectedLean,
294
+ ok: baseline.status === expectedBaseline && lean.status === expectedLean,
295
+ lean_findings: lean.lean_review
296
+ });
297
+ }
298
+ const overbuildRows = rows.filter((row) => row.group === 'overbuild');
299
+ const safetyRows = rows.filter((row) => row.group === 'safety');
300
+ const report = {
301
+ schema: 'sks.lean-policy-bench.v1',
302
+ generated_at: nowIso(),
303
+ policy_id: LEAN_ENGINEERING_POLICY_ID,
304
+ policy_hash: LEAN_ENGINEERING_POLICY_HASH,
305
+ method: 'hermetic fake Codex SDK comparison of baseline context versus lean-policy context; no live model accuracy or production speed claim',
306
+ arms: ['baseline-context-fixture', 'lean-policy-context'],
307
+ ok: rows.every((row) => row.ok),
308
+ metrics: {
309
+ scenario_count: rows.length,
310
+ overbuild_scenarios: overbuildRows.length,
311
+ safety_scenarios: safetyRows.length,
312
+ overbuild_caught_by_lean: overbuildRows.filter((row) => row.lean_status === 'needs_more_work').length,
313
+ safety_rejected_by_both: safetyRows.filter((row) => row.baseline_status === 'rejected' && row.lean_status === 'rejected').length,
314
+ dependencies_added: 0
315
+ },
316
+ scenarios: rows
317
+ };
318
+ await writeLeanPolicyBenchArtifacts(root, report);
319
+ return report;
320
+ }
321
+ async function fakeGptFinalScenario(candidate, leanEnabled) {
322
+ const prompt = [
323
+ leanEnabled ? leanEngineeringCompactText() : 'Baseline implementation context without lean policy.',
324
+ leanEnabled ? 'Lean review: evaluate over-build, dependency, fallback, root-cause, and validation safety.' : 'Review only catastrophic safety issues.',
325
+ `Candidate: ${candidate}`
326
+ ].join('\n');
327
+ const result = await runFakeCodexSdkTask({
328
+ route: '$Bench',
329
+ tier: 'orchestrator',
330
+ missionId: 'lean-policy-bench',
331
+ workItemId: 'lean-policy-bench',
332
+ slotId: 'lean-policy-bench',
333
+ generationIndex: 1,
334
+ sessionId: 'lean-policy-bench',
335
+ cwd: process.cwd(),
336
+ prompt,
337
+ inputFiles: [],
338
+ inputImages: [],
339
+ outputSchemaId: GPT_FINAL_ARBITER_RESULT_SCHEMA_ID,
340
+ outputSchema: gptFinalArbiterResultSchema,
341
+ sandboxPolicy: 'read-only',
342
+ requestedScopeContract: {
343
+ id: 'lean-policy-bench',
344
+ route: '$Bench',
345
+ read_only: true,
346
+ allowed_paths: [],
347
+ write_paths: [],
348
+ user_confirmed_full_access: false,
349
+ mad_sks_authorized: false
350
+ },
351
+ mutationLedgerRoot: rootForBench(),
352
+ reliabilityPolicy: {
353
+ maxEmptyResultRetries: 0,
354
+ timeoutClass: 'fast'
355
+ }
356
+ });
357
+ return result.structuredOutput || {};
358
+ }
359
+ function rootForBench() {
360
+ return path.join(os.tmpdir(), 'sks-lean-policy-bench');
361
+ }
362
+ async function writeLeanPolicyBenchArtifacts(root, report) {
363
+ const dir = path.join(root, '.sneakoscope', 'reports', 'performance');
364
+ await ensureDir(dir);
365
+ await writeJsonAtomic(path.join(dir, 'lean-policy-bench.json'), report);
366
+ const lines = [
367
+ '# SKS Lean Policy Bench',
368
+ '',
369
+ `Generated: ${report.generated_at}`,
370
+ `Status: ${report.ok ? 'pass' : 'blocked'}`,
371
+ `Policy: ${report.policy_id} (${report.policy_hash})`,
372
+ '',
373
+ report.method,
374
+ '',
375
+ '| Scenario | Group | Baseline | Lean | Status |',
376
+ '| --- | --- | --- | --- | --- |'
377
+ ];
378
+ for (const row of report.scenarios)
379
+ lines.push(`| \`${row.id}\` | ${row.group} | ${row.baseline_status} | ${row.lean_status} | ${row.ok ? 'pass' : 'blocked'} |`);
380
+ await writeTextAtomic(path.join(dir, 'lean-policy-bench.md'), `${lines.join('\n')}\n`);
381
+ }
267
382
  //# sourceMappingURL=bench.js.map
@@ -1,29 +1,43 @@
1
1
  import path from 'node:path';
2
2
  import fsp from 'node:fs/promises';
3
+ import { spawnSync } from 'node:child_process';
3
4
  import { nowIso, writeJsonAtomic } from './fsx.js';
5
+ import { LEAN_CHANGE_EVIDENCE_SCHEMA, leanPolicyReference, parseLeanSimplificationMarkerLine } from './lean-engineering-policy.js';
4
6
  export const CODE_STRUCTURE_THRESHOLDS = {
5
7
  warning: 1000,
6
8
  review: 2000,
7
9
  split_required_review: 3000
8
10
  };
9
- const DEFAULT_INCLUDE = new Set(['.js', '.js', '.ts', '.tsx', '.jsx', '.cjs']);
11
+ const DEFAULT_INCLUDE = new Set(['.js', '.ts', '.tsx', '.jsx', '.cjs', '.mjs']);
10
12
  const SKIP_DIRS = new Set(['.git', 'node_modules', '.sneakoscope', 'dist', 'build', 'coverage']);
13
+ const SOURCE_DIR_RE = /^(src|test|tests|schemas|scripts|crates|docs)\//;
14
+ const SOURCE_EXT_RE = /\.(js|ts|tsx|jsx|cjs|mjs|json|md|rs|toml)$/;
15
+ const FALLBACK_RE = /\b(fallback|legacy|shim|compat|mock|catch-all|catch all|default provider)\b/i;
16
+ const CONFIG_FLAG_RE = /\b(process\.env|SKS_[A-Z0-9_]+|CODEX_[A-Z0-9_]+|[A-Z][A-Z0-9_]{5,})\b/;
17
+ const ABSTRACTION_RE = /\b(interface|abstract class|class|factory|provider|adapter|registry|orchestrator|manager)\b/;
11
18
  export async function scanCodeStructure(root, opts = {}) {
12
- const files = opts.files?.length ? opts.files.map((file) => path.resolve(root, file)) : await listSourceFiles(root);
13
- const touched = new Set((opts.touchedFiles || []).map((file) => path.relative(root, path.resolve(root, file))));
19
+ const changedScope = await collectChangedScope(root, opts);
20
+ const files = await resolveScanFiles(root, opts, changedScope);
21
+ const touched = new Set((opts.touchedFiles || []).map((file) => normalizeRel(root, file)));
22
+ const changedSet = new Set((changedScope.source_files || []).map((file) => normalizeSlashes(file)));
14
23
  const entries = [];
24
+ const intentionalSimplifications = [];
15
25
  for (const file of files) {
16
- const rel = path.relative(root, file);
17
- const lineCount = await countLines(file).catch(() => 0);
26
+ const rel = normalizeRel(root, file);
27
+ const text = await fsp.readFile(file, 'utf8').catch(() => '');
28
+ const lineCount = text ? text.split(/\n/).length : 0;
18
29
  const status = structureStatus(lineCount);
19
- if (status === 'ok' && !opts.includeOk)
30
+ const changedByDiff = changedSet.has(rel);
31
+ if (status === 'ok' && !opts.includeOk && !changedByDiff)
20
32
  continue;
33
+ const signals = analyzeTextSignals(rel, text, changedByDiff);
34
+ intentionalSimplifications.push(...signals.lean_markers);
21
35
  entries.push({
22
36
  path: rel,
23
37
  line_count: lineCount,
24
38
  status,
25
39
  generated_or_vendor: isGeneratedOrVendor(rel),
26
- touched_by_mission: touched.size ? touched.has(rel) : false,
40
+ touched_by_mission: touched.size ? touched.has(rel) : changedByDiff,
27
41
  recommended_action: recommendedAction(rel, lineCount),
28
42
  exception: lineCount >= CODE_STRUCTURE_THRESHOLDS.split_required_review && !isGeneratedOrVendor(rel)
29
43
  ? {
@@ -34,15 +48,43 @@ export async function scanCodeStructure(root, opts = {}) {
34
48
  next_split_candidate: nextSplitCandidate(rel),
35
49
  temporary_until: 'next substantial edit to this file'
36
50
  }
37
- : null
51
+ : null,
52
+ lean_signals: signals
38
53
  });
39
54
  }
55
+ const dependencyDelta = await collectDependencyDelta(root, changedScope.base);
56
+ const fallbackSites = await collectAddedFallbackSites(root, changedScope);
57
+ const runnableChecks = collectRunnableChecks(changedScope);
58
+ const semanticReview = buildSemanticReview({
59
+ entries,
60
+ changedScope,
61
+ dependencyDelta,
62
+ fallbackSites,
63
+ intentionalSimplifications,
64
+ runnableChecks
65
+ });
66
+ const leanChangeEvidence = buildLeanChangeEvidence({
67
+ changedScope,
68
+ dependencyDelta,
69
+ fallbackSites,
70
+ intentionalSimplifications,
71
+ runnableChecks,
72
+ semanticReview
73
+ });
40
74
  return {
41
75
  schema_version: 1,
42
76
  mission_id: opts.missionId || null,
43
77
  scanned_at: nowIso(),
44
78
  thresholds: CODE_STRUCTURE_THRESHOLDS,
45
79
  files: entries.sort((a, b) => b.line_count - a.line_count),
80
+ changed_scope: changedScope,
81
+ dependencies_added: dependencyDelta.added,
82
+ dependencies_removed: dependencyDelta.removed,
83
+ fallback_sites_added: fallbackSites,
84
+ intentional_simplifications: intentionalSimplifications,
85
+ runnable_checks: runnableChecks,
86
+ semantic_review: semanticReview,
87
+ lean_change_evidence: leanChangeEvidence,
46
88
  actions_taken: opts.actions_taken || [],
47
89
  remaining_risks: entries.filter((entry) => entry.exception).map((entry) => `${entry.path}: ${entry.status}`)
48
90
  };
@@ -52,6 +94,103 @@ export async function writeCodeStructureReport(root, dir, opts = {}) {
52
94
  await writeJsonAtomic(path.join(dir, 'code-structure-report.json'), report);
53
95
  return report;
54
96
  }
97
+ export function leanChangeEvidenceFromReport(report) {
98
+ if (report?.lean_change_evidence)
99
+ return report.lean_change_evidence;
100
+ return buildLeanChangeEvidence({
101
+ changedScope: report?.changed_scope || emptyChangedScope('unknown', 'HEAD'),
102
+ dependencyDelta: {
103
+ added: report?.dependencies_added || [],
104
+ removed: report?.dependencies_removed || []
105
+ },
106
+ fallbackSites: report?.fallback_sites_added || [],
107
+ intentionalSimplifications: report?.intentional_simplifications || [],
108
+ runnableChecks: report?.runnable_checks || [],
109
+ semanticReview: report?.semantic_review || { status: 'needs-review', findings: [] }
110
+ });
111
+ }
112
+ async function resolveScanFiles(root, opts, changedScope) {
113
+ if (opts.files?.length)
114
+ return opts.files.map((file) => path.resolve(root, file));
115
+ const changedSourceFiles = (changedScope.source_files || [])
116
+ .filter((file) => DEFAULT_INCLUDE.has(path.extname(file)))
117
+ .map((file) => path.resolve(root, file));
118
+ if ((opts.changed || opts.changedSince || opts.changedFiles?.length) && changedSourceFiles.length)
119
+ return changedSourceFiles;
120
+ return listSourceFiles(root);
121
+ }
122
+ async function collectChangedScope(root, opts) {
123
+ if (opts.changedFiles?.length) {
124
+ const changedFiles = Array.from(new Set(opts.changedFiles.map((file) => normalizeRel(root, file))));
125
+ return {
126
+ ...emptyChangedScope('explicit', opts.changedSince || 'HEAD'),
127
+ changed_files: changedFiles,
128
+ source_files: changedFiles.filter(isSourceLike),
129
+ entries: changedFiles.map((file) => ({ path: file, status: 'M', lines_added: 0, lines_deleted: 0 }))
130
+ };
131
+ }
132
+ const shouldCollect = opts.changed || opts.changedSince;
133
+ if (!shouldCollect)
134
+ return emptyChangedScope('full', opts.changedSince || 'HEAD');
135
+ const base = String(opts.changedSince || (typeof opts.changed === 'string' ? opts.changed : 'HEAD'));
136
+ const numstat = gitLines(root, ['diff', '--numstat', base, '--']);
137
+ const nameStatus = gitLines(root, ['diff', '--name-status', base, '--']);
138
+ const untracked = gitLines(root, ['ls-files', '--others', '--exclude-standard']);
139
+ const entriesByPath = new Map();
140
+ for (const line of numstat) {
141
+ const parts = line.split(/\t/);
142
+ if (parts.length < 3)
143
+ continue;
144
+ const linesAdded = parseNumstat(parts[0] || '0');
145
+ const linesDeleted = parseNumstat(parts[1] || '0');
146
+ const rel = normalizeSlashes(parts.slice(2).join('\t'));
147
+ entriesByPath.set(rel, {
148
+ path: rel,
149
+ status: 'M',
150
+ lines_added: linesAdded,
151
+ lines_deleted: linesDeleted
152
+ });
153
+ }
154
+ for (const line of nameStatus) {
155
+ const parts = line.split(/\t/).filter(Boolean);
156
+ if (parts.length < 2)
157
+ continue;
158
+ const status = parts[0];
159
+ const rel = normalizeSlashes(parts[parts.length - 1] || '');
160
+ if (!rel)
161
+ continue;
162
+ const existing = entriesByPath.get(rel) || { path: rel, lines_added: 0, lines_deleted: 0 };
163
+ entriesByPath.set(rel, { ...existing, status });
164
+ }
165
+ for (const rel of untracked.map(normalizeSlashes).filter(Boolean)) {
166
+ if (entriesByPath.has(rel))
167
+ continue;
168
+ const text = await fsp.readFile(path.join(root, rel), 'utf8').catch(() => '');
169
+ entriesByPath.set(rel, {
170
+ path: rel,
171
+ status: 'A',
172
+ lines_added: text ? text.split(/\n/).length : 0,
173
+ lines_deleted: 0
174
+ });
175
+ }
176
+ const entries = [...entriesByPath.values()].filter((entry) => entry.path);
177
+ const changedFiles = entries.map((entry) => entry.path);
178
+ const sourceFiles = changedFiles.filter(isSourceLike);
179
+ const linesAdded = entries.reduce((sum, entry) => sum + Number(entry.lines_added || 0), 0);
180
+ const linesDeleted = entries.reduce((sum, entry) => sum + Number(entry.lines_deleted || 0), 0);
181
+ return {
182
+ mode: 'git-diff',
183
+ base,
184
+ changed_files: changedFiles,
185
+ files_added: entries.filter((entry) => String(entry.status || '').startsWith('A')).length,
186
+ files_deleted: entries.filter((entry) => String(entry.status || '').startsWith('D')).length,
187
+ lines_added: linesAdded,
188
+ lines_deleted: linesDeleted,
189
+ net_lines: linesAdded - linesDeleted,
190
+ source_files: sourceFiles,
191
+ entries
192
+ };
193
+ }
55
194
  async function listSourceFiles(root, dir = root, out = []) {
56
195
  const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => []);
57
196
  for (const entry of entries) {
@@ -70,6 +209,203 @@ async function listSourceFiles(root, dir = root, out = []) {
70
209
  }
71
210
  return out;
72
211
  }
212
+ async function collectDependencyDelta(root, base = 'HEAD') {
213
+ const current = await readPackageDependencyNames(path.join(root, 'package.json'));
214
+ const previousText = gitText(root, ['show', `${base}:package.json`]);
215
+ const previous = parsePackageDependencyNames(previousText || '{}');
216
+ return {
217
+ added: [...current].filter((name) => !previous.has(name)).sort(),
218
+ removed: [...previous].filter((name) => !current.has(name)).sort()
219
+ };
220
+ }
221
+ async function readPackageDependencyNames(file) {
222
+ const text = await fsp.readFile(file, 'utf8').catch(() => '{}');
223
+ return parsePackageDependencyNames(text);
224
+ }
225
+ function parsePackageDependencyNames(text) {
226
+ const value = safeJson(text);
227
+ const names = new Set();
228
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
229
+ const deps = value?.[section] && typeof value[section] === 'object' ? value[section] : {};
230
+ for (const name of Object.keys(deps))
231
+ names.add(`${section}:${name}`);
232
+ }
233
+ return names;
234
+ }
235
+ async function collectAddedFallbackSites(root, changedScope) {
236
+ if (!changedScope?.source_files?.length || changedScope.mode !== 'git-diff')
237
+ return [];
238
+ const diff = gitText(root, ['diff', '--unified=0', changedScope.base || 'HEAD', '--', ...changedScope.source_files]);
239
+ const sites = [];
240
+ let currentFile = '';
241
+ let currentLine = 0;
242
+ for (const line of diff.split(/\r?\n/)) {
243
+ if (line.startsWith('+++ b/')) {
244
+ currentFile = normalizeSlashes(line.slice('+++ b/'.length));
245
+ continue;
246
+ }
247
+ const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
248
+ if (hunk) {
249
+ currentLine = Number(hunk[1]) || 0;
250
+ continue;
251
+ }
252
+ if (line.startsWith('+') && !line.startsWith('+++')) {
253
+ if (FALLBACK_RE.test(line))
254
+ sites.push({ file: currentFile, line: currentLine, text: line.slice(1).trim().slice(0, 160) });
255
+ currentLine += 1;
256
+ continue;
257
+ }
258
+ if (!line.startsWith('-'))
259
+ currentLine += 1;
260
+ }
261
+ return sites;
262
+ }
263
+ function analyzeTextSignals(rel, text, changedByDiff) {
264
+ const lines = text.split(/\r?\n/);
265
+ const imports = lines.filter((line) => /^\s*import\s/.test(line));
266
+ const externalImports = imports
267
+ .map((line) => /from\s+['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/.exec(line)?.[1] || /from\s+['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/.exec(line)?.[2] || '')
268
+ .filter((specifier) => specifier && !specifier.startsWith('.') && !specifier.startsWith('node:'));
269
+ const leanMarkers = lines
270
+ .map((line, index) => parseLeanSimplificationMarkerLine(line, rel, index + 1))
271
+ .filter((marker) => Boolean(marker));
272
+ const effectiveLines = lines.map((line) => line.trim()).filter(Boolean);
273
+ const forwardingOnly = effectiveLines.length > 0
274
+ && effectiveLines.length <= 10
275
+ && effectiveLines.every((line) => line.startsWith('import ') || line.startsWith('export ') || line.startsWith('//') || line.startsWith('/*') || line.startsWith('*'));
276
+ return {
277
+ import_count: imports.length,
278
+ external_dependency_imports: Array.from(new Set(externalImports)).sort(),
279
+ ts_nocheck: /^\s*\/\/\s*@ts-nocheck\b/m.test(text),
280
+ changed_by_diff: changedByDiff,
281
+ forwarding_only: forwardingOnly,
282
+ fallback_markers: countMatches(text, FALLBACK_RE),
283
+ config_flag_markers: countMatches(text, CONFIG_FLAG_RE),
284
+ abstraction_markers: countMatches(text, ABSTRACTION_RE),
285
+ lean_markers: leanMarkers
286
+ };
287
+ }
288
+ function collectRunnableChecks(changedScope) {
289
+ return (changedScope.source_files || [])
290
+ .filter((file) => /\b(test|tests|fixture|fixtures|spec|check|__tests__)\b/i.test(file))
291
+ .sort();
292
+ }
293
+ function buildSemanticReview(input) {
294
+ const findings = [];
295
+ for (const dep of input.dependencyDelta.added || []) {
296
+ findings.push({
297
+ tag: 'reuse',
298
+ severity: 'blocker',
299
+ summary: `new dependency requires explicit lean justification: ${dep}`
300
+ });
301
+ }
302
+ if ((input.fallbackSites || []).length) {
303
+ findings.push({
304
+ tag: 'fallback',
305
+ severity: 'review',
306
+ summary: `${input.fallbackSites.length} added fallback/compat/mock marker(s) need authority and proof`
307
+ });
308
+ }
309
+ const changedEntries = (input.entries || input.changedScope?.entries || []);
310
+ if (input.changedScope?.net_lines > 300) {
311
+ findings.push({
312
+ tag: 'shrink',
313
+ severity: 'review',
314
+ summary: `changed diff is +${input.changedScope.net_lines} net lines; confirm this is the smallest sufficient change`
315
+ });
316
+ }
317
+ if ((input.changedScope?.source_files || []).length && !(input.runnableChecks || []).length) {
318
+ findings.push({
319
+ tag: 'verify',
320
+ severity: 'review',
321
+ summary: 'changed source files were detected without a changed runnable check file'
322
+ });
323
+ }
324
+ for (const entry of input.entries || []) {
325
+ if (!entry.lean_signals?.changed_by_diff)
326
+ continue;
327
+ if (entry.lean_signals.ts_nocheck) {
328
+ findings.push({
329
+ tag: 'verify',
330
+ severity: isLeanOwnedTypeSafetyPath(entry.path) ? 'blocker' : 'review',
331
+ file: entry.path,
332
+ summary: isLeanOwnedTypeSafetyPath(entry.path)
333
+ ? 'changed Lean/architecture-owned file contains @ts-nocheck'
334
+ : 'changed auxiliary fixture/gate file contains @ts-nocheck; keep typed migration scoped'
335
+ });
336
+ }
337
+ if (entry.lean_signals.forwarding_only) {
338
+ findings.push({
339
+ tag: 'reuse',
340
+ severity: 'review',
341
+ file: entry.path,
342
+ summary: 'changed file is forwarding-only; confirm it replaces an older path instead of duplicating an SSOT'
343
+ });
344
+ }
345
+ if (entry.lean_signals.config_flag_markers > 6 || entry.lean_signals.abstraction_markers > 12) {
346
+ findings.push({
347
+ tag: 'yagni',
348
+ severity: 'review',
349
+ file: entry.path,
350
+ summary: 'changed file has dense config/abstraction markers; review for unrequested knobs or layers'
351
+ });
352
+ }
353
+ }
354
+ for (const marker of input.intentionalSimplifications || []) {
355
+ if (marker.status === 'complete')
356
+ continue;
357
+ findings.push({
358
+ tag: 'shrink',
359
+ severity: 'review',
360
+ file: marker.file,
361
+ line: marker.line,
362
+ summary: `lean simplification marker is incomplete: ${marker.status}`
363
+ });
364
+ }
365
+ const status = findings.some((finding) => finding.severity === 'blocker')
366
+ ? 'blocked'
367
+ : findings.some((finding) => finding.severity === 'review')
368
+ ? 'needs-review'
369
+ : 'pass';
370
+ return { status, findings };
371
+ }
372
+ function isLeanOwnedTypeSafetyPath(file) {
373
+ return [
374
+ 'src/core/code-structure.ts',
375
+ 'src/core/commands/code-structure-command.ts',
376
+ 'src/core/lean-engineering-policy.ts',
377
+ 'src/core/codex-control/gpt-final-arbiter.ts',
378
+ 'src/core/codex-control/gpt-final-review-schema.ts',
379
+ 'src/core/codex-control/codex-fake-sdk-adapter.ts',
380
+ 'src/core/agents/native-worker-backend-router.ts',
381
+ 'src/scripts/check-architecture.ts',
382
+ 'src/scripts/check-command-module-budget.ts',
383
+ 'src/scripts/check-pipeline-budget.ts',
384
+ 'src/scripts/check-route-modularity.ts',
385
+ 'src/scripts/check-publish-tag.ts',
386
+ 'src/scripts/gpt-final-arbiter-check.ts',
387
+ 'src/scripts/release-registry-check.ts'
388
+ ].includes(file);
389
+ }
390
+ function buildLeanChangeEvidence(input) {
391
+ const changedScope = input.changedScope || emptyChangedScope('unknown', 'HEAD');
392
+ return {
393
+ schema: LEAN_CHANGE_EVIDENCE_SCHEMA,
394
+ ...leanPolicyReference(),
395
+ changed_files: changedScope.changed_files || [],
396
+ files_added: changedScope.files_added || 0,
397
+ files_deleted: changedScope.files_deleted || 0,
398
+ lines_added: changedScope.lines_added || 0,
399
+ lines_deleted: changedScope.lines_deleted || 0,
400
+ net_lines: changedScope.net_lines || 0,
401
+ dependencies_added: input.dependencyDelta?.added || [],
402
+ dependencies_removed: input.dependencyDelta?.removed || [],
403
+ fallback_sites_added: input.fallbackSites || [],
404
+ intentional_simplifications: input.intentionalSimplifications || [],
405
+ runnable_checks: input.runnableChecks || [],
406
+ semantic_review: input.semanticReview || { status: 'needs-review', findings: [] }
407
+ };
408
+ }
73
409
  async function countLines(file) {
74
410
  const text = await fsp.readFile(file, 'utf8');
75
411
  return text ? text.split(/\n/).length : 0;
@@ -89,17 +425,69 @@ function isGeneratedOrVendor(rel) {
89
425
  function recommendedAction(rel, lines) {
90
426
  if (lines < CODE_STRUCTURE_THRESHOLDS.warning)
91
427
  return 'none';
92
- if (/src\/cli\/main\.js$/.test(rel))
428
+ if (/src\/cli\/main\.(js|ts)$/.test(rel))
93
429
  return 'extract CLI subcommand handlers into focused modules before adding substantial command logic';
94
430
  if (/routes|pipeline|init/.test(rel))
95
431
  return 'extract policy tables or route-specific execution into focused modules';
96
432
  return 'identify a cohesive module boundary and extract before adding unrelated logic';
97
433
  }
98
434
  function nextSplitCandidate(rel) {
99
- if (/src\/cli\/main\.js$/.test(rel))
435
+ if (/src\/cli\/main\.(js|ts)$/.test(rel))
100
436
  return 'goal/wiki/team/eval/db command handlers';
101
- if (/src\/core\/pipeline\.js$/.test(rel))
437
+ if (/src\/core\/pipeline\.(js|ts)$/.test(rel))
102
438
  return 'route prepare handlers and stop-gate evaluators';
103
439
  return 'largest cohesive command or policy section';
104
440
  }
441
+ function countMatches(text, pattern) {
442
+ const flags = pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`;
443
+ return [...text.matchAll(new RegExp(pattern.source, flags))].length;
444
+ }
445
+ function isSourceLike(file) {
446
+ const rel = normalizeSlashes(file);
447
+ return SOURCE_DIR_RE.test(rel) && SOURCE_EXT_RE.test(rel) && !isGeneratedOrVendor(rel);
448
+ }
449
+ function parseNumstat(value) {
450
+ const parsed = Number(value);
451
+ return Number.isFinite(parsed) ? parsed : 0;
452
+ }
453
+ function safeJson(text) {
454
+ try {
455
+ return JSON.parse(text);
456
+ }
457
+ catch {
458
+ return {};
459
+ }
460
+ }
461
+ function gitLines(root, args) {
462
+ const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
463
+ if (result.status !== 0)
464
+ return [];
465
+ return String(result.stdout || '').split(/\r?\n/).filter(Boolean);
466
+ }
467
+ function gitText(root, args) {
468
+ const result = spawnSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
469
+ if (result.status !== 0)
470
+ return '';
471
+ return String(result.stdout || '');
472
+ }
473
+ function emptyChangedScope(mode, base) {
474
+ return {
475
+ mode,
476
+ base,
477
+ changed_files: [],
478
+ files_added: 0,
479
+ files_deleted: 0,
480
+ lines_added: 0,
481
+ lines_deleted: 0,
482
+ net_lines: 0,
483
+ source_files: [],
484
+ entries: []
485
+ };
486
+ }
487
+ function normalizeRel(root, file) {
488
+ return normalizeSlashes(path.relative(root, path.resolve(root, file)));
489
+ }
490
+ function normalizeSlashes(file) {
491
+ return String(file || '').replace(/\\/g, '/');
492
+ }
105
493
  //# sourceMappingURL=code-structure.js.map