audit-tools 0.41.1 → 0.42.1

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 (209) hide show
  1. package/dist/audit/cli/args.d.ts +0 -1
  2. package/dist/audit/cli/args.d.ts.map +1 -1
  3. package/dist/audit/cli/args.js +0 -7
  4. package/dist/audit/cli/args.js.map +1 -1
  5. package/dist/audit/cli/auditStep.d.ts +18 -0
  6. package/dist/audit/cli/auditStep.d.ts.map +1 -1
  7. package/dist/audit/cli/auditStep.js +35 -1
  8. package/dist/audit/cli/auditStep.js.map +1 -1
  9. package/dist/audit/cli/cleanup.d.ts +1 -0
  10. package/dist/audit/cli/cleanup.d.ts.map +1 -1
  11. package/dist/audit/cli/cleanup.js +22 -7
  12. package/dist/audit/cli/cleanup.js.map +1 -1
  13. package/dist/audit/cli/confirmIntentStep.d.ts.map +1 -1
  14. package/dist/audit/cli/confirmIntentStep.js +11 -0
  15. package/dist/audit/cli/confirmIntentStep.js.map +1 -1
  16. package/dist/audit/cli/dispatch/hostHandoff.d.ts.map +1 -1
  17. package/dist/audit/cli/dispatch/hostHandoff.js +19 -1
  18. package/dist/audit/cli/dispatch/hostHandoff.js.map +1 -1
  19. package/dist/audit/cli/envelope.d.ts +0 -24
  20. package/dist/audit/cli/envelope.d.ts.map +1 -1
  21. package/dist/audit/cli/envelope.js +0 -18
  22. package/dist/audit/cli/envelope.js.map +1 -1
  23. package/dist/audit/cli/fanoutLanes.d.ts +24 -0
  24. package/dist/audit/cli/fanoutLanes.d.ts.map +1 -1
  25. package/dist/audit/cli/fanoutLanes.js +46 -2
  26. package/dist/audit/cli/fanoutLanes.js.map +1 -1
  27. package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
  28. package/dist/audit/cli/nextStepCommand.js +19 -24
  29. package/dist/audit/cli/nextStepCommand.js.map +1 -1
  30. package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
  31. package/dist/audit/cli/nextStepHelpers.js +20 -6
  32. package/dist/audit/cli/nextStepHelpers.js.map +1 -1
  33. package/dist/audit/cli/workerResult.d.ts +0 -13
  34. package/dist/audit/cli/workerResult.d.ts.map +1 -1
  35. package/dist/audit/cli/workerResult.js +0 -15
  36. package/dist/audit/cli/workerResult.js.map +1 -1
  37. package/dist/audit/contracts/workerSchemas.d.ts +13 -83
  38. package/dist/audit/contracts/workerSchemas.d.ts.map +1 -1
  39. package/dist/audit/contracts/workerSchemas.js +10 -4
  40. package/dist/audit/contracts/workerSchemas.js.map +1 -1
  41. package/dist/audit/orchestrator/advance.d.ts +30 -0
  42. package/dist/audit/orchestrator/advance.d.ts.map +1 -1
  43. package/dist/audit/orchestrator/advance.js +45 -2
  44. package/dist/audit/orchestrator/advance.js.map +1 -1
  45. package/dist/audit/orchestrator/designReviewPrompt.d.ts.map +1 -1
  46. package/dist/audit/orchestrator/designReviewPrompt.js +6 -4
  47. package/dist/audit/orchestrator/designReviewPrompt.js.map +1 -1
  48. package/dist/audit/orchestrator/intentCheckpointExecutor.d.ts +8 -0
  49. package/dist/audit/orchestrator/intentCheckpointExecutor.d.ts.map +1 -1
  50. package/dist/audit/orchestrator/intentCheckpointExecutor.js +2 -0
  51. package/dist/audit/orchestrator/intentCheckpointExecutor.js.map +1 -1
  52. package/dist/audit/orchestrator/partitionTaskGraph.d.ts.map +1 -1
  53. package/dist/audit/orchestrator/partitionTaskGraph.js +2 -2
  54. package/dist/audit/orchestrator/partitionTaskGraph.js.map +1 -1
  55. package/dist/audit/orchestrator/reviewPacketSizing.d.ts +0 -1
  56. package/dist/audit/orchestrator/reviewPacketSizing.d.ts.map +1 -1
  57. package/dist/audit/orchestrator/reviewPacketSizing.js +0 -5
  58. package/dist/audit/orchestrator/reviewPacketSizing.js.map +1 -1
  59. package/dist/audit/orchestrator/reviewPackets.d.ts +8 -2
  60. package/dist/audit/orchestrator/reviewPackets.d.ts.map +1 -1
  61. package/dist/audit/orchestrator/reviewPackets.js +7 -8
  62. package/dist/audit/orchestrator/reviewPackets.js.map +1 -1
  63. package/dist/audit/orchestrator/taskAffinityGraph.d.ts +0 -6
  64. package/dist/audit/orchestrator/taskAffinityGraph.d.ts.map +1 -1
  65. package/dist/audit/orchestrator/taskAffinityGraph.js +0 -16
  66. package/dist/audit/orchestrator/taskAffinityGraph.js.map +1 -1
  67. package/dist/audit/orchestrator/taskBuilder.d.ts.map +1 -1
  68. package/dist/audit/orchestrator/taskBuilder.js +1 -2
  69. package/dist/audit/orchestrator/taskBuilder.js.map +1 -1
  70. package/dist/audit/reporting/synthesis.d.ts.map +1 -1
  71. package/dist/audit/reporting/synthesis.js +2 -3
  72. package/dist/audit/reporting/synthesis.js.map +1 -1
  73. package/dist/audit/reporting/workBlocks.d.ts +0 -1
  74. package/dist/audit/reporting/workBlocks.d.ts.map +1 -1
  75. package/dist/audit/reporting/workBlocks.js +4 -41
  76. package/dist/audit/reporting/workBlocks.js.map +1 -1
  77. package/dist/audit/systemic/systemicChallengeLoop.d.ts +0 -8
  78. package/dist/audit/systemic/systemicChallengeLoop.d.ts.map +1 -1
  79. package/dist/audit/systemic/systemicChallengeLoop.js +0 -9
  80. package/dist/audit/systemic/systemicChallengeLoop.js.map +1 -1
  81. package/dist/audit/types.d.ts +4 -4
  82. package/dist/remediate/contractPipeline/artifactStore.d.ts +19 -23
  83. package/dist/remediate/contractPipeline/artifactStore.d.ts.map +1 -1
  84. package/dist/remediate/contractPipeline/artifactStore.js +22 -29
  85. package/dist/remediate/contractPipeline/artifactStore.js.map +1 -1
  86. package/dist/remediate/contractPipeline/changeClassification.d.ts +0 -12
  87. package/dist/remediate/contractPipeline/changeClassification.d.ts.map +1 -1
  88. package/dist/remediate/contractPipeline/changeClassification.js +0 -101
  89. package/dist/remediate/contractPipeline/changeClassification.js.map +1 -1
  90. package/dist/remediate/contractPipeline/derive.d.ts +0 -42
  91. package/dist/remediate/contractPipeline/derive.d.ts.map +1 -1
  92. package/dist/remediate/contractPipeline/derive.js +0 -108
  93. package/dist/remediate/contractPipeline/derive.js.map +1 -1
  94. package/dist/remediate/index.d.ts +0 -1
  95. package/dist/remediate/index.d.ts.map +1 -1
  96. package/dist/remediate/index.js +5 -132
  97. package/dist/remediate/index.js.map +1 -1
  98. package/dist/remediate/intake.d.ts +0 -55
  99. package/dist/remediate/intake.d.ts.map +1 -1
  100. package/dist/remediate/intake.js +1 -81
  101. package/dist/remediate/intake.js.map +1 -1
  102. package/dist/remediate/phases/plan.d.ts +0 -1
  103. package/dist/remediate/phases/plan.d.ts.map +1 -1
  104. package/dist/remediate/phases/plan.js +1 -207
  105. package/dist/remediate/phases/plan.js.map +1 -1
  106. package/dist/remediate/state/types.d.ts +4 -4
  107. package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
  108. package/dist/remediate/steps/contractPipeline.js +15 -12
  109. package/dist/remediate/steps/contractPipeline.js.map +1 -1
  110. package/dist/remediate/steps/contractPipelinePrompts.d.ts +9 -9
  111. package/dist/remediate/steps/contractPipelinePrompts.d.ts.map +1 -1
  112. package/dist/remediate/steps/contractPipelinePrompts.js +26 -18
  113. package/dist/remediate/steps/contractPipelinePrompts.js.map +1 -1
  114. package/dist/remediate/steps/dispatch/hostHandoff.d.ts +0 -1
  115. package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
  116. package/dist/remediate/steps/dispatch/hostHandoff.js +3 -2
  117. package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
  118. package/dist/remediate/steps/dispatch.d.ts +0 -8
  119. package/dist/remediate/steps/dispatch.d.ts.map +1 -1
  120. package/dist/remediate/steps/dispatch.js +3 -9
  121. package/dist/remediate/steps/dispatch.js.map +1 -1
  122. package/dist/remediate/steps/nextStep.d.ts +0 -1
  123. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  124. package/dist/remediate/steps/nextStep.js +0 -1
  125. package/dist/remediate/steps/nextStep.js.map +1 -1
  126. package/dist/remediate/utils/fileIntegrity.d.ts +0 -8
  127. package/dist/remediate/utils/fileIntegrity.d.ts.map +1 -1
  128. package/dist/remediate/utils/fileIntegrity.js +5 -65
  129. package/dist/remediate/utils/fileIntegrity.js.map +1 -1
  130. package/dist/remediate/validation/contractPipelineGates.d.ts.map +1 -1
  131. package/dist/remediate/validation/contractPipelineGates.js +27 -19
  132. package/dist/remediate/validation/contractPipelineGates.js.map +1 -1
  133. package/dist/shared/analyzers/candidates.d.ts +6 -6
  134. package/dist/shared/analyzers/candidates.d.ts.map +1 -1
  135. package/dist/shared/analyzers/candidates.js +6 -6
  136. package/dist/shared/analyzers/candidates.js.map +1 -1
  137. package/dist/shared/chunkByBudget.d.ts +8 -8
  138. package/dist/shared/decompose/contentCoherence.d.ts +43 -273
  139. package/dist/shared/decompose/contentCoherence.d.ts.map +1 -1
  140. package/dist/shared/decompose/contentCoherence.js +120 -63
  141. package/dist/shared/decompose/contentCoherence.js.map +1 -1
  142. package/dist/shared/decompose/modularity.d.ts +17 -0
  143. package/dist/shared/decompose/modularity.d.ts.map +1 -1
  144. package/dist/shared/decompose/modularity.js +78 -3
  145. package/dist/shared/decompose/modularity.js.map +1 -1
  146. package/dist/shared/decompose/systemicChallenge.d.ts +4 -4
  147. package/dist/shared/decompose/workBlockSeams.d.ts +45 -0
  148. package/dist/shared/decompose/workBlockSeams.d.ts.map +1 -0
  149. package/dist/shared/decompose/workBlockSeams.js +82 -0
  150. package/dist/shared/decompose/workBlockSeams.js.map +1 -0
  151. package/dist/shared/index.d.ts +6 -5
  152. package/dist/shared/index.d.ts.map +1 -1
  153. package/dist/shared/index.js +6 -4
  154. package/dist/shared/index.js.map +1 -1
  155. package/dist/shared/io/auditToolsPaths.d.ts +0 -20
  156. package/dist/shared/io/auditToolsPaths.d.ts.map +1 -1
  157. package/dist/shared/io/auditToolsPaths.js +0 -24
  158. package/dist/shared/io/auditToolsPaths.js.map +1 -1
  159. package/dist/shared/loopCorePaths.d.ts.map +1 -1
  160. package/dist/shared/loopCorePaths.js +15 -9
  161. package/dist/shared/loopCorePaths.js.map +1 -1
  162. package/dist/shared/reporting/auditDeliverable.d.ts.map +1 -1
  163. package/dist/shared/reporting/auditDeliverable.js +8 -3
  164. package/dist/shared/reporting/auditDeliverable.js.map +1 -1
  165. package/dist/shared/types/finding.d.ts +119 -317
  166. package/dist/shared/types/finding.d.ts.map +1 -1
  167. package/dist/shared/types/finding.js +38 -11
  168. package/dist/shared/types/finding.js.map +1 -1
  169. package/dist/shared/validation/findingGrounding.d.ts +11 -0
  170. package/dist/shared/validation/findingGrounding.d.ts.map +1 -1
  171. package/dist/shared/validation/findingGrounding.js +21 -0
  172. package/dist/shared/validation/findingGrounding.js.map +1 -1
  173. package/dist/shared/validation/findingsReport.d.ts.map +1 -1
  174. package/dist/shared/validation/findingsReport.js +19 -7
  175. package/dist/shared/validation/findingsReport.js.map +1 -1
  176. package/docs/audit-pkg/contracts.md +2 -2
  177. package/docs/audit-pkg/development.md +1 -1
  178. package/docs/audit-pkg/release.md +20 -114
  179. package/package.json +6 -2
  180. package/schemas/audit_result.schema.json +0 -20
  181. package/schemas/audit_results.schema.json +0 -20
  182. package/schemas/finding.schema.json +0 -20
  183. package/skills/audit-code/SKILL.md +3 -7
  184. package/skills/audit-code/audit-code.prompt.md +5 -9
  185. package/skills/remediate-code/SKILL.md +0 -1
  186. package/dist/remediate/steps/dispatch/common.d.ts +0 -126
  187. package/dist/remediate/steps/dispatch/common.d.ts.map +0 -1
  188. package/dist/remediate/steps/dispatch/common.js +0 -204
  189. package/dist/remediate/steps/dispatch/common.js.map +0 -1
  190. package/dist/remediate/steps/dispatch/dagNodeFields.d.ts +0 -25
  191. package/dist/remediate/steps/dispatch/dagNodeFields.d.ts.map +0 -1
  192. package/dist/remediate/steps/dispatch/dagNodeFields.js +0 -5
  193. package/dist/remediate/steps/dispatch/dagNodeFields.js.map +0 -1
  194. package/dist/remediate/steps/dispatch/implementPrompt.d.ts +0 -47
  195. package/dist/remediate/steps/dispatch/implementPrompt.d.ts.map +0 -1
  196. package/dist/remediate/steps/dispatch/implementPrompt.js +0 -165
  197. package/dist/remediate/steps/dispatch/implementPrompt.js.map +0 -1
  198. package/dist/remediate/steps/dispatch/verifyCommands.d.ts +0 -144
  199. package/dist/remediate/steps/dispatch/verifyCommands.d.ts.map +0 -1
  200. package/dist/remediate/steps/dispatch/verifyCommands.js +0 -331
  201. package/dist/remediate/steps/dispatch/verifyCommands.js.map +0 -1
  202. package/dist/remediate/steps/dispatch/writeScope.d.ts +0 -43
  203. package/dist/remediate/steps/dispatch/writeScope.d.ts.map +0 -1
  204. package/dist/remediate/steps/dispatch/writeScope.js +0 -81
  205. package/dist/remediate/steps/dispatch/writeScope.js.map +0 -1
  206. package/dist/remediate/utils/hostAssets.d.ts +0 -21
  207. package/dist/remediate/utils/hostAssets.d.ts.map +0 -1
  208. package/dist/remediate/utils/hostAssets.js +0 -34
  209. package/dist/remediate/utils/hostAssets.js.map +0 -1
@@ -15,6 +15,39 @@ export declare const CONTENT_COHERENCE_SCORES: Readonly<{
15
15
  readonly shared_unit: 80;
16
16
  }>;
17
17
  export declare const CONTENT_COHERENCE_THRESHOLD = 60;
18
+ /**
19
+ * Which pairs may join at all — the ONE policy axis of the shared core, never a
20
+ * fork. Both draws run the identical scan, union-find, and canonical ordering;
21
+ * they differ only in this predicate and in {@link ContentCoherencePolicy.refineAtModularityPeak}.
22
+ *
23
+ * - `weighted_score_threshold` — a pair joins when its summed evidence weight
24
+ * reaches {@link CONTENT_COHERENCE_THRESHOLD}. Disjunctive: four of the six
25
+ * classes clear 60 alone.
26
+ * - `shared_file_and_same_lens` — a pair joins only when it shares a FILE **and**
27
+ * a lens. Measured on the promoted 2026-08-18 run (3,230 findings): the
28
+ * threshold rule left 99.97% of findings in a single component, and no
29
+ * class-COUNT variant fixed it, because `shared_unit`/`same_lens`/`same_directory`
30
+ * are near-vacuous partitions at audit scale. The conjunction bounds by
31
+ * STRUCTURE, not density — `same_lens` is a hard partition no edge may cross.
32
+ */
33
+ export type ContentCoherenceEligibility = "weighted_score_threshold" | "shared_file_and_same_lens";
34
+ /** The per-draw policy. Every difference between the draws lands here. */
35
+ export interface ContentCoherencePolicy {
36
+ readonly eligibility: ContentCoherenceEligibility;
37
+ /**
38
+ * Refine each eligible-edge component at its modularity peak (below), so a
39
+ * loose component splits at its own data-derived thin seam while a true clique
40
+ * survives whole. No budget, ceiling, or size target participates.
41
+ */
42
+ readonly refineAtModularityPeak: boolean;
43
+ }
44
+ /**
45
+ * The audit TASK draw. Unmeasured for collapse — its eligibility is deliberately
46
+ * left as-is until it is measured on its own lap (`docs/backlog/open-bugs.md`).
47
+ */
48
+ export declare const TASK_DRAW_COHERENCE_POLICY: ContentCoherencePolicy;
49
+ /** The FINDINGS draw (work blocks + the findings deliverable). Owner cut, 2026-08-19. */
50
+ export declare const FINDINGS_DRAW_COHERENCE_POLICY: ContentCoherencePolicy;
18
51
  export type ContentCoherenceEvidence = {
19
52
  -readonly [Kind in keyof typeof CONTENT_COHERENCE_SCORES]: boolean;
20
53
  };
@@ -43,52 +76,10 @@ export interface NormalizedContentCoherenceItem {
43
76
  tags: string[];
44
77
  critical_flow_ids?: string[];
45
78
  }
46
- export interface ContentCoherencePairScore {
47
- left: string;
48
- right: string;
49
- evidence: ContentCoherenceEvidence;
50
- score: number;
51
- eligible: boolean;
52
- }
53
- export interface ContentCoherenceCandidate {
54
- left: string;
55
- right: string;
56
- score: number;
57
- }
58
- export interface ContentCoherenceMergeTraceEntry extends ContentCoherenceCandidate {
59
- decision: "merge" | "already_connected";
60
- root: string;
61
- }
62
79
  export interface ContentCoherenceTrace {
63
80
  normalized_items: NormalizedContentCoherenceItem[];
64
- pair_scores: ContentCoherencePairScore[];
65
- eligible_candidates: ContentCoherenceCandidate[];
66
- merge_trace: ContentCoherenceMergeTraceEntry[];
67
- merge_decisions: Array<ContentCoherenceMergeTraceEntry["decision"]>;
68
81
  components: string[][];
69
82
  }
70
- export declare const ContentCoherenceEvidenceSchema: z.ZodObject<{
71
- call_import_reference_adjacency: z.ZodBoolean;
72
- same_directory: z.ZodBoolean;
73
- shared_critical_flow: z.ZodBoolean;
74
- shared_file: z.ZodBoolean;
75
- shared_semantic_tag_or_same_lens: z.ZodBoolean;
76
- shared_unit: z.ZodBoolean;
77
- }, "strict", z.ZodTypeAny, {
78
- call_import_reference_adjacency: boolean;
79
- same_directory: boolean;
80
- shared_critical_flow: boolean;
81
- shared_file: boolean;
82
- shared_semantic_tag_or_same_lens: boolean;
83
- shared_unit: boolean;
84
- }, {
85
- call_import_reference_adjacency: boolean;
86
- same_directory: boolean;
87
- shared_critical_flow: boolean;
88
- shared_file: boolean;
89
- shared_semantic_tag_or_same_lens: boolean;
90
- shared_unit: boolean;
91
- }>;
92
83
  export declare const NormalizedContentCoherenceItemSchema: z.ZodObject<{
93
84
  id: z.ZodString;
94
85
  file_paths: z.ZodArray<z.ZodString, "many">;
@@ -108,93 +99,6 @@ export declare const NormalizedContentCoherenceItemSchema: z.ZodObject<{
108
99
  tags: string[];
109
100
  critical_flow_ids?: string[] | undefined;
110
101
  }>;
111
- export declare const ContentCoherencePairScoreSchema: z.ZodObject<{
112
- left: z.ZodString;
113
- right: z.ZodString;
114
- evidence: z.ZodObject<{
115
- call_import_reference_adjacency: z.ZodBoolean;
116
- same_directory: z.ZodBoolean;
117
- shared_critical_flow: z.ZodBoolean;
118
- shared_file: z.ZodBoolean;
119
- shared_semantic_tag_or_same_lens: z.ZodBoolean;
120
- shared_unit: z.ZodBoolean;
121
- }, "strict", z.ZodTypeAny, {
122
- call_import_reference_adjacency: boolean;
123
- same_directory: boolean;
124
- shared_critical_flow: boolean;
125
- shared_file: boolean;
126
- shared_semantic_tag_or_same_lens: boolean;
127
- shared_unit: boolean;
128
- }, {
129
- call_import_reference_adjacency: boolean;
130
- same_directory: boolean;
131
- shared_critical_flow: boolean;
132
- shared_file: boolean;
133
- shared_semantic_tag_or_same_lens: boolean;
134
- shared_unit: boolean;
135
- }>;
136
- score: z.ZodNumber;
137
- eligible: z.ZodBoolean;
138
- }, "strict", z.ZodTypeAny, {
139
- left: string;
140
- right: string;
141
- evidence: {
142
- call_import_reference_adjacency: boolean;
143
- same_directory: boolean;
144
- shared_critical_flow: boolean;
145
- shared_file: boolean;
146
- shared_semantic_tag_or_same_lens: boolean;
147
- shared_unit: boolean;
148
- };
149
- score: number;
150
- eligible: boolean;
151
- }, {
152
- left: string;
153
- right: string;
154
- evidence: {
155
- call_import_reference_adjacency: boolean;
156
- same_directory: boolean;
157
- shared_critical_flow: boolean;
158
- shared_file: boolean;
159
- shared_semantic_tag_or_same_lens: boolean;
160
- shared_unit: boolean;
161
- };
162
- score: number;
163
- eligible: boolean;
164
- }>;
165
- export declare const ContentCoherenceCandidateSchema: z.ZodObject<{
166
- left: z.ZodString;
167
- right: z.ZodString;
168
- score: z.ZodNumber;
169
- }, "strict", z.ZodTypeAny, {
170
- left: string;
171
- right: string;
172
- score: number;
173
- }, {
174
- left: string;
175
- right: string;
176
- score: number;
177
- }>;
178
- export declare const ContentCoherenceMergeTraceEntrySchema: z.ZodObject<{
179
- left: z.ZodString;
180
- right: z.ZodString;
181
- score: z.ZodNumber;
182
- } & {
183
- decision: z.ZodEnum<["merge", "already_connected"]>;
184
- root: z.ZodString;
185
- }, "strict", z.ZodTypeAny, {
186
- decision: "merge" | "already_connected";
187
- left: string;
188
- right: string;
189
- score: number;
190
- root: string;
191
- }, {
192
- decision: "merge" | "already_connected";
193
- left: string;
194
- right: string;
195
- score: number;
196
- root: string;
197
- }>;
198
102
  export declare const ContentCoherenceTraceSchema: z.ZodObject<{
199
103
  normalized_items: z.ZodArray<z.ZodObject<{
200
104
  id: z.ZodString;
@@ -215,94 +119,6 @@ export declare const ContentCoherenceTraceSchema: z.ZodObject<{
215
119
  tags: string[];
216
120
  critical_flow_ids?: string[] | undefined;
217
121
  }>, "many">;
218
- pair_scores: z.ZodArray<z.ZodObject<{
219
- left: z.ZodString;
220
- right: z.ZodString;
221
- evidence: z.ZodObject<{
222
- call_import_reference_adjacency: z.ZodBoolean;
223
- same_directory: z.ZodBoolean;
224
- shared_critical_flow: z.ZodBoolean;
225
- shared_file: z.ZodBoolean;
226
- shared_semantic_tag_or_same_lens: z.ZodBoolean;
227
- shared_unit: z.ZodBoolean;
228
- }, "strict", z.ZodTypeAny, {
229
- call_import_reference_adjacency: boolean;
230
- same_directory: boolean;
231
- shared_critical_flow: boolean;
232
- shared_file: boolean;
233
- shared_semantic_tag_or_same_lens: boolean;
234
- shared_unit: boolean;
235
- }, {
236
- call_import_reference_adjacency: boolean;
237
- same_directory: boolean;
238
- shared_critical_flow: boolean;
239
- shared_file: boolean;
240
- shared_semantic_tag_or_same_lens: boolean;
241
- shared_unit: boolean;
242
- }>;
243
- score: z.ZodNumber;
244
- eligible: z.ZodBoolean;
245
- }, "strict", z.ZodTypeAny, {
246
- left: string;
247
- right: string;
248
- evidence: {
249
- call_import_reference_adjacency: boolean;
250
- same_directory: boolean;
251
- shared_critical_flow: boolean;
252
- shared_file: boolean;
253
- shared_semantic_tag_or_same_lens: boolean;
254
- shared_unit: boolean;
255
- };
256
- score: number;
257
- eligible: boolean;
258
- }, {
259
- left: string;
260
- right: string;
261
- evidence: {
262
- call_import_reference_adjacency: boolean;
263
- same_directory: boolean;
264
- shared_critical_flow: boolean;
265
- shared_file: boolean;
266
- shared_semantic_tag_or_same_lens: boolean;
267
- shared_unit: boolean;
268
- };
269
- score: number;
270
- eligible: boolean;
271
- }>, "many">;
272
- eligible_candidates: z.ZodArray<z.ZodObject<{
273
- left: z.ZodString;
274
- right: z.ZodString;
275
- score: z.ZodNumber;
276
- }, "strict", z.ZodTypeAny, {
277
- left: string;
278
- right: string;
279
- score: number;
280
- }, {
281
- left: string;
282
- right: string;
283
- score: number;
284
- }>, "many">;
285
- merge_trace: z.ZodArray<z.ZodObject<{
286
- left: z.ZodString;
287
- right: z.ZodString;
288
- score: z.ZodNumber;
289
- } & {
290
- decision: z.ZodEnum<["merge", "already_connected"]>;
291
- root: z.ZodString;
292
- }, "strict", z.ZodTypeAny, {
293
- decision: "merge" | "already_connected";
294
- left: string;
295
- right: string;
296
- score: number;
297
- root: string;
298
- }, {
299
- decision: "merge" | "already_connected";
300
- left: string;
301
- right: string;
302
- score: number;
303
- root: string;
304
- }>, "many">;
305
- merge_decisions: z.ZodArray<z.ZodEnum<["merge", "already_connected"]>, "many">;
306
122
  components: z.ZodArray<z.ZodArray<z.ZodString, "many">, "many">;
307
123
  }, "strict", z.ZodTypeAny, {
308
124
  normalized_items: {
@@ -312,33 +128,6 @@ export declare const ContentCoherenceTraceSchema: z.ZodObject<{
312
128
  tags: string[];
313
129
  critical_flow_ids?: string[] | undefined;
314
130
  }[];
315
- pair_scores: {
316
- left: string;
317
- right: string;
318
- evidence: {
319
- call_import_reference_adjacency: boolean;
320
- same_directory: boolean;
321
- shared_critical_flow: boolean;
322
- shared_file: boolean;
323
- shared_semantic_tag_or_same_lens: boolean;
324
- shared_unit: boolean;
325
- };
326
- score: number;
327
- eligible: boolean;
328
- }[];
329
- eligible_candidates: {
330
- left: string;
331
- right: string;
332
- score: number;
333
- }[];
334
- merge_trace: {
335
- decision: "merge" | "already_connected";
336
- left: string;
337
- right: string;
338
- score: number;
339
- root: string;
340
- }[];
341
- merge_decisions: ("merge" | "already_connected")[];
342
131
  components: string[][];
343
132
  }, {
344
133
  normalized_items: {
@@ -348,35 +137,16 @@ export declare const ContentCoherenceTraceSchema: z.ZodObject<{
348
137
  tags: string[];
349
138
  critical_flow_ids?: string[] | undefined;
350
139
  }[];
351
- pair_scores: {
352
- left: string;
353
- right: string;
354
- evidence: {
355
- call_import_reference_adjacency: boolean;
356
- same_directory: boolean;
357
- shared_critical_flow: boolean;
358
- shared_file: boolean;
359
- shared_semantic_tag_or_same_lens: boolean;
360
- shared_unit: boolean;
361
- };
362
- score: number;
363
- eligible: boolean;
364
- }[];
365
- eligible_candidates: {
366
- left: string;
367
- right: string;
368
- score: number;
369
- }[];
370
- merge_trace: {
371
- decision: "merge" | "already_connected";
372
- left: string;
373
- right: string;
374
- score: number;
375
- root: string;
376
- }[];
377
- merge_decisions: ("merge" | "already_connected")[];
378
140
  components: string[][];
379
141
  }>;
380
- /** Build the complete canonical trace used by both audit and findings draws. */
381
- export declare function buildContentCoherenceTrace(input: ContentCoherenceInput | Record<string, unknown>): ContentCoherenceTrace;
142
+ /**
143
+ * Build the complete canonical trace used by both audit and findings draws.
144
+ *
145
+ * `policy` is REQUIRED and has no default: a draw that forgot to declare its
146
+ * eligibility would otherwise silently inherit the other draw's, which is
147
+ * exactly the class of latent failure this project refuses to leave to a
148
+ * caller's memory. Pass {@link TASK_DRAW_COHERENCE_POLICY} or
149
+ * {@link FINDINGS_DRAW_COHERENCE_POLICY}.
150
+ */
151
+ export declare function buildContentCoherenceTrace(input: ContentCoherenceInput | Record<string, unknown>, policy: ContentCoherencePolicy): ContentCoherenceTrace;
382
152
  //# sourceMappingURL=contentCoherence.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contentCoherence.d.ts","sourceRoot":"","sources":["../../../src/shared/decompose/contentCoherence.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"contentCoherence.d.ts","sourceRoot":"","sources":["../../../src/shared/decompose/contentCoherence.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,wBAAwB;;;;;;;EAO1B,CAAC;AAEZ,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,2BAA2B,GACnC,0BAA0B,GAC1B,2BAA2B,CAAC;AAEhC,0EAA0E;AAC1E,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,WAAW,EAAE,2BAA2B,CAAC;IAClD;;;;OAIG;IACH,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;CAC1C;AAED;;;GAGG;AACH,eAAO,MAAM,0BAA0B,EAAE,sBAGvC,CAAC;AAEH,yFAAyF;AACzF,eAAO,MAAM,8BAA8B,EAAE,sBAG3C,CAAC;AAiBH,MAAM,MAAM,wBAAwB,GAAG;IACrC,CAAC,UAAU,IAAI,IAAI,MAAM,OAAO,wBAAwB,GAAG,OAAO;CACnE,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACjD;AAED,MAAM,MAAM,gCAAgC,GACxC,aAAa,GACb,sBAAsB,GACtB,WAAW,GACX,eAAe,GACf,WAAW,GACX,WAAW,GACX,UAAU,CAAC;AAEf,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,gCAAgC,GAAG,MAAM,CAAC;CACjD;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACvC,aAAa,CAAC,EAAE,SAAS,4BAA4B,EAAE,CAAC;CACzD;AAED,MAAM,WAAW,8BAA8B;IAC7C,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B;AAoBD,MAAM,WAAW,qBAAqB;IACpC,gBAAgB,EAAE,8BAA8B,EAAE,CAAC;IACnD,UAAU,EAAE,MAAM,EAAE,EAAE,CAAC;CACxB;AAED,eAAO,MAAM,oCAAoC;;;;;;;;;;;;;;;;;;EAQtC,CAAC;AAEZ,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAK7B,CAAC;AA4RZ;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,qBAAqB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtD,MAAM,EAAE,sBAAsB,GAC7B,qBAAqB,CAkGvB"}
@@ -6,6 +6,7 @@
6
6
  * those components.
7
7
  */
8
8
  import { z } from "zod";
9
+ import { louvain, modularityOf } from "./modularity.js";
9
10
  export const CONTENT_COHERENCE_SCORES = Object.freeze({
10
11
  call_import_reference_adjacency: 70,
11
12
  same_directory: 10,
@@ -15,16 +16,32 @@ export const CONTENT_COHERENCE_SCORES = Object.freeze({
15
16
  shared_unit: 80,
16
17
  });
17
18
  export const CONTENT_COHERENCE_THRESHOLD = 60;
18
- export const ContentCoherenceEvidenceSchema = z
19
- .object({
20
- call_import_reference_adjacency: z.boolean(),
21
- same_directory: z.boolean(),
22
- shared_critical_flow: z.boolean(),
23
- shared_file: z.boolean(),
24
- shared_semantic_tag_or_same_lens: z.boolean(),
25
- shared_unit: z.boolean(),
26
- })
27
- .strict();
19
+ /**
20
+ * The audit TASK draw. Unmeasured for collapse — its eligibility is deliberately
21
+ * left as-is until it is measured on its own lap (`docs/backlog/open-bugs.md`).
22
+ */
23
+ export const TASK_DRAW_COHERENCE_POLICY = Object.freeze({
24
+ eligibility: "weighted_score_threshold",
25
+ refineAtModularityPeak: false,
26
+ });
27
+ /** The FINDINGS draw (work blocks + the findings deliverable). Owner cut, 2026-08-19. */
28
+ export const FINDINGS_DRAW_COHERENCE_POLICY = Object.freeze({
29
+ eligibility: "shared_file_and_same_lens",
30
+ refineAtModularityPeak: true,
31
+ });
32
+ /**
33
+ * The canonical modularity resolution (γ). NOT a tuned knob and never to become
34
+ * one: γ = 1 is the literature default, at which the trivial whole-component
35
+ * partition scores exactly 0, so "did the split beat keeping the component
36
+ * whole" is answered by the data alone.
37
+ */
38
+ const MODULARITY_RESOLUTION = 1;
39
+ /**
40
+ * Components at or below this size are handed back unrefined. A triviality
41
+ * guard, not a size budget: modularity over one, two, or three nodes describes
42
+ * noise rather than structure, and nothing downstream reads the number.
43
+ */
44
+ const TRIVIAL_COMPONENT_SIZE = 3;
28
45
  export const NormalizedContentCoherenceItemSchema = z
29
46
  .object({
30
47
  id: z.string().min(1),
@@ -34,33 +51,9 @@ export const NormalizedContentCoherenceItemSchema = z
34
51
  critical_flow_ids: z.array(z.string()).optional(),
35
52
  })
36
53
  .strict();
37
- export const ContentCoherencePairScoreSchema = z
38
- .object({
39
- left: z.string(),
40
- right: z.string(),
41
- evidence: ContentCoherenceEvidenceSchema,
42
- score: z.number().int(),
43
- eligible: z.boolean(),
44
- })
45
- .strict();
46
- export const ContentCoherenceCandidateSchema = z
47
- .object({
48
- left: z.string(),
49
- right: z.string(),
50
- score: z.number().int(),
51
- })
52
- .strict();
53
- export const ContentCoherenceMergeTraceEntrySchema = ContentCoherenceCandidateSchema.extend({
54
- decision: z.enum(["merge", "already_connected"]),
55
- root: z.string(),
56
- }).strict();
57
54
  export const ContentCoherenceTraceSchema = z
58
55
  .object({
59
56
  normalized_items: z.array(NormalizedContentCoherenceItemSchema),
60
- pair_scores: z.array(ContentCoherencePairScoreSchema),
61
- eligible_candidates: z.array(ContentCoherenceCandidateSchema),
62
- merge_trace: z.array(ContentCoherenceMergeTraceEntrySchema),
63
- merge_decisions: z.array(z.enum(["merge", "already_connected"])),
64
57
  components: z.array(z.array(z.string())),
65
58
  })
66
59
  .strict();
@@ -201,6 +194,55 @@ function scoreEvidence(evidence) {
201
194
  }
202
195
  return score;
203
196
  }
197
+ function isEligible(direct, score, policy) {
198
+ return policy.eligibility === "shared_file_and_same_lens"
199
+ ? direct.sharedFile && direct.sameLens
200
+ : score >= CONTENT_COHERENCE_THRESHOLD;
201
+ }
202
+ /**
203
+ * Split one eligible-edge component at its modularity PEAK, or hand it back
204
+ * whole. The component's own eligible pairs are the weighted graph (edge weight =
205
+ * the pair's evidence score), Louvain proposes a partition at the canonical γ,
206
+ * and the proposal is accepted only when it scores strictly above keeping the
207
+ * component whole. A component held together by one weak bridge splits there.
208
+ * Nothing in the decision is a constant of any denomination.
209
+ *
210
+ * What survives whole is a UNIFORM-weight clique: with every edge equal, any cut
211
+ * scores below the trivial partition, so it cannot be split. A clique whose
212
+ * weights DIFFER can still split — six findings on one file and one lens but two
213
+ * units weigh 220 within a unit and 140 across it, and that contrast alone beats
214
+ * the whole. That is intended (the halves are genuinely less coupled), and the
215
+ * shared file then surfaces as a seam between them rather than disappearing into
216
+ * one block. "A clique stays whole" without the uniform qualifier is an
217
+ * overclaim; the two cases are pinned separately in
218
+ * `tests/shared/content-coherence.test.ts`.
219
+ */
220
+ function refineAtModularityPeak(members, candidates) {
221
+ if (members.length <= TRIVIAL_COMPONENT_SIZE)
222
+ return [[...members]];
223
+ const graph = {
224
+ nodes: [...members],
225
+ edges: candidates.map((candidate) => ({
226
+ a: candidate.left,
227
+ b: candidate.right,
228
+ weight: candidate.score,
229
+ })),
230
+ };
231
+ const proposed = louvain(graph, MODULARITY_RESOLUTION);
232
+ const whole = new Map(members.map((id) => [id, members[0] ?? id]));
233
+ if (modularityOf(graph, proposed, MODULARITY_RESOLUTION) <=
234
+ modularityOf(graph, whole, MODULARITY_RESOLUTION)) {
235
+ return [[...members]];
236
+ }
237
+ const communities = new Map();
238
+ for (const member of members) {
239
+ const community = proposed.get(member) ?? member;
240
+ const group = communities.get(community) ?? [];
241
+ group.push(member);
242
+ communities.set(community, group);
243
+ }
244
+ return [...communities.values()];
245
+ }
204
246
  function findRoot(parent, id) {
205
247
  const lineage = [];
206
248
  let root = id;
@@ -212,37 +254,50 @@ function findRoot(parent, id) {
212
254
  parent.set(member, root);
213
255
  return root;
214
256
  }
215
- /** Build the complete canonical trace used by both audit and findings draws. */
216
- export function buildContentCoherenceTrace(input) {
257
+ /**
258
+ * Build the complete canonical trace used by both audit and findings draws.
259
+ *
260
+ * `policy` is REQUIRED and has no default: a draw that forgot to declare its
261
+ * eligibility would otherwise silently inherit the other draw's, which is
262
+ * exactly the class of latent failure this project refuses to leave to a
263
+ * caller's memory. Pass {@link TASK_DRAW_COHERENCE_POLICY} or
264
+ * {@link FINDINGS_DRAW_COHERENCE_POLICY}.
265
+ */
266
+ export function buildContentCoherenceTrace(input, policy) {
217
267
  if (!isRecord(input)) {
218
268
  throw new Error("Content-coherence input must be an object.");
219
269
  }
220
270
  const normalizedItems = normalizeItems(input.items);
221
271
  const itemIds = new Set(normalizedItems.map((item) => item.id));
222
272
  const relationshipEvidence = normalizeRelationshipEvidence(input.relationships, itemIds);
223
- const pairScores = [];
273
+ // Only ELIGIBLE pairs are retained. The ineligible majority (82% of pairs on
274
+ // the 3,194-finding dogfood run) can affect nothing downstream — union-find
275
+ // consumes eligibility alone — so materializing the full O(N²) pair layer was
276
+ // pure cost. Scoring itself is unchanged; the sort comparator below is total
277
+ // over distinct (left,right) pairs, so filtering during the scan yields the
278
+ // byte-identical candidate order the filter-then-sort form produced.
279
+ const eligibleCandidates = [];
224
280
  for (let leftIndex = 0; leftIndex < normalizedItems.length; leftIndex += 1) {
225
281
  for (let rightIndex = leftIndex + 1; rightIndex < normalizedItems.length; rightIndex += 1) {
226
282
  const left = normalizedItems[leftIndex];
227
283
  const right = normalizedItems[rightIndex];
228
284
  const evidence = directEvidence(left, right);
285
+ // Captured BEFORE relationship evidence is folded in — the conjunctive
286
+ // policy must read only what the items themselves assert.
287
+ const direct = {
288
+ sharedFile: evidence.shared_file,
289
+ sameLens: evidence.shared_semantic_tag_or_same_lens,
290
+ };
229
291
  for (const kind of relationshipEvidence.get(pairKey(left.id, right.id)) ?? []) {
230
292
  evidence[kind] = true;
231
293
  }
232
294
  const score = scoreEvidence(evidence);
233
- pairScores.push({
234
- left: left.id,
235
- right: right.id,
236
- evidence,
237
- score,
238
- eligible: score >= CONTENT_COHERENCE_THRESHOLD,
239
- });
295
+ if (!isEligible(direct, score, policy))
296
+ continue;
297
+ eligibleCandidates.push({ left: left.id, right: right.id, score });
240
298
  }
241
299
  }
242
- const eligibleCandidates = pairScores
243
- .filter((pair) => pair.eligible)
244
- .map(({ left, right, score }) => ({ left, right, score }))
245
- .sort((left, right) => {
300
+ eligibleCandidates.sort((left, right) => {
246
301
  if (left.score !== right.score)
247
302
  return right.score - left.score;
248
303
  const leftDelta = compareCodeUnits(left.left, right.left);
@@ -251,22 +306,14 @@ export function buildContentCoherenceTrace(input) {
251
306
  : compareCodeUnits(left.right, right.right);
252
307
  });
253
308
  const parent = new Map(normalizedItems.map((item) => [item.id, item.id]));
254
- const mergeTrace = [];
255
309
  for (const candidate of eligibleCandidates) {
256
310
  const leftRoot = findRoot(parent, candidate.left);
257
311
  const rightRoot = findRoot(parent, candidate.right);
258
- if (leftRoot === rightRoot) {
259
- mergeTrace.push({
260
- ...candidate,
261
- decision: "already_connected",
262
- root: leftRoot,
263
- });
312
+ if (leftRoot === rightRoot)
264
313
  continue;
265
- }
266
314
  const root = compareCodeUnits(leftRoot, rightRoot) <= 0 ? leftRoot : rightRoot;
267
315
  const other = root === leftRoot ? rightRoot : leftRoot;
268
316
  parent.set(other, root);
269
- mergeTrace.push({ ...candidate, decision: "merge", root });
270
317
  }
271
318
  const groups = new Map();
272
319
  for (const item of normalizedItems) {
@@ -275,15 +322,25 @@ export function buildContentCoherenceTrace(input) {
275
322
  members.push(item.id);
276
323
  groups.set(root, members);
277
324
  }
278
- const components = [...groups.values()]
279
- .map((members) => members.sort(compareCodeUnits))
325
+ // Bucket the eligible pairs by the component they ended up inside, so
326
+ // refinement reads each component's own weighted evidence graph once.
327
+ const candidatesByRoot = new Map();
328
+ if (policy.refineAtModularityPeak) {
329
+ for (const candidate of eligibleCandidates) {
330
+ const root = findRoot(parent, candidate.left);
331
+ const bucket = candidatesByRoot.get(root) ?? [];
332
+ bucket.push(candidate);
333
+ candidatesByRoot.set(root, bucket);
334
+ }
335
+ }
336
+ const merged = [...groups.entries()].map(([root, members]) => [root, members.sort(compareCodeUnits)]);
337
+ const components = (policy.refineAtModularityPeak
338
+ ? merged.flatMap(([root, members]) => refineAtModularityPeak(members, candidatesByRoot.get(root) ?? []))
339
+ : merged.map(([, members]) => members))
340
+ .map((members) => [...members].sort(compareCodeUnits))
280
341
  .sort((left, right) => compareCodeUnits(left[0] ?? "", right[0] ?? ""));
281
342
  return {
282
343
  normalized_items: normalizedItems,
283
- pair_scores: pairScores,
284
- eligible_candidates: eligibleCandidates,
285
- merge_trace: mergeTrace,
286
- merge_decisions: mergeTrace.map((entry) => entry.decision),
287
344
  components,
288
345
  };
289
346
  }