@3fn/core 13.0.0 → 14.1.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 (284) hide show
  1. package/.kiro/agents/ada-prompt.md +80 -132
  2. package/.kiro/agents/ada-prompt.md.attribution.json +45 -0
  3. package/.kiro/agents/ada.json +44 -59
  4. package/.kiro/agents/ada.json.attribution.json +13 -0
  5. package/.kiro/agents/data-prompt.md +83 -74
  6. package/.kiro/agents/data-prompt.md.attribution.json +53 -0
  7. package/.kiro/agents/data.json +31 -38
  8. package/.kiro/agents/data.json.attribution.json +13 -0
  9. package/.kiro/agents/kenya-prompt.md +83 -72
  10. package/.kiro/agents/kenya-prompt.md.attribution.json +53 -0
  11. package/.kiro/agents/kenya.json +27 -35
  12. package/.kiro/agents/kenya.json.attribution.json +13 -0
  13. package/.kiro/agents/leonardo-prompt.md +176 -234
  14. package/.kiro/agents/leonardo-prompt.md.attribution.json +45 -0
  15. package/.kiro/agents/leonardo.json +28 -36
  16. package/.kiro/agents/leonardo.json.attribution.json +13 -0
  17. package/.kiro/agents/lina-prompt.md +110 -151
  18. package/.kiro/agents/lina-prompt.md.attribution.json +53 -0
  19. package/.kiro/agents/lina.json +46 -59
  20. package/.kiro/agents/lina.json.attribution.json +13 -0
  21. package/.kiro/agents/sparky-prompt.md +89 -72
  22. package/.kiro/agents/sparky-prompt.md.attribution.json +53 -0
  23. package/.kiro/agents/sparky.json +37 -37
  24. package/.kiro/agents/sparky.json.attribution.json +13 -0
  25. package/.kiro/agents/stacy-prompt.md +74 -48
  26. package/.kiro/agents/stacy-prompt.md.attribution.json +45 -0
  27. package/.kiro/agents/stacy.json +28 -30
  28. package/.kiro/agents/stacy.json.attribution.json +13 -0
  29. package/.kiro/agents/thurgood-prompt.md +94 -138
  30. package/.kiro/agents/thurgood-prompt.md.attribution.json +45 -0
  31. package/.kiro/agents/thurgood.json +31 -35
  32. package/.kiro/agents/thurgood.json.attribution.json +13 -0
  33. package/.kiro/steering/AI-Collaboration-Principles.md +3 -3
  34. package/.kiro/steering/Civitas-System-Overview.md +7 -16
  35. package/.kiro/steering/DesignerPunk-Systems-Overview.md +6 -6
  36. package/.kiro/steering/Spec-Feedback-Protocol.md +2 -11
  37. package/.kiro/steering/Task-Completion-Protocol.md +98 -17
  38. package/.kiro/steering/core-goals.md +3 -3
  39. package/.kiro/steering/personal-note.md +1 -1
  40. package/.kiro/steering/start-up-tasks.md +17 -6
  41. package/application-mcp-server/src/index.ts +26 -0
  42. package/dist/ComponentTokens.android.kt +12 -12
  43. package/dist/ComponentTokens.ios.swift +12 -12
  44. package/dist/ComponentTokens.web.css +3 -3
  45. package/dist/DesignTokens.android.kt +1 -1
  46. package/dist/DesignTokens.dtcg.json +8 -5
  47. package/dist/DesignTokens.figma.json +2 -2
  48. package/dist/DesignTokens.ios.swift +1 -1
  49. package/dist/DesignTokens.web.css +1 -1
  50. package/dist/android/DesignTokens.android.kt +1 -1
  51. package/dist/blend/OklchBlendCalculator.js +1 -0
  52. package/dist/blend/ThemeAwareBlendUtilities.web.d.ts +13 -2
  53. package/dist/blend/ThemeAwareBlendUtilities.web.js +6 -1
  54. package/dist/browser/designerpunk.esm.js +36 -90
  55. package/dist/browser/designerpunk.esm.min.js +33 -36
  56. package/dist/browser/designerpunk.umd.js +36 -90
  57. package/dist/browser/designerpunk.umd.min.js +47 -50
  58. package/dist/browser/tokens.css +3 -3
  59. package/dist/build/tokens/defineComponentTokens.d.ts +10 -0
  60. package/dist/build/tokens/defineComponentTokens.js +26 -0
  61. package/dist/components/core/Avatar-Base/avatar.tokens.d.ts +21 -26
  62. package/dist/components/core/Avatar-Base/avatar.tokens.js +31 -34
  63. package/dist/components/core/Avatar-Base/index.d.ts +1 -1
  64. package/dist/components/core/Avatar-Base/index.js +2 -2
  65. package/dist/components/core/Avatar-Base/platforms/web/Avatar.web.js +24 -5
  66. package/dist/components/core/Button-CTA/examples/BasicUsage.d.ts +16 -28
  67. package/dist/components/core/Button-CTA/examples/BasicUsage.js +18 -43
  68. package/dist/components/core/Button-CTA/platforms/web/ButtonCTA.web.d.ts +3 -15
  69. package/dist/components/core/Button-CTA/platforms/web/ButtonCTA.web.js +9 -58
  70. package/dist/components/core/Button-CTA/types.d.ts +0 -24
  71. package/dist/components/core/Button-CTA/types.js +6 -0
  72. package/dist/components/core/Button-Icon/buttonIcon.tokens.d.ts +28 -14
  73. package/dist/components/core/Button-Icon/buttonIcon.tokens.js +35 -20
  74. package/dist/components/core/Input-Text-Base/types.d.ts +13 -1
  75. package/dist/components/core/Input-Text-Password/platforms/web/InputTextPassword.web.js +11 -2
  76. package/dist/generators/DTCGFormatGenerator.js +8 -0
  77. package/dist/generators/TokenFileGenerator.js +7 -2
  78. package/dist/integration/BuildErrorHandler.js +2 -2
  79. package/dist/ios/DesignTokens.ios.swift +1 -1
  80. package/dist/mcp/application-mcp.js +24 -0
  81. package/dist/mcp/docs-mcp.js +130 -15
  82. package/dist/mcp/product-mcp.js +25 -0
  83. package/dist/tokens/OpacityTokens.js +1 -1
  84. package/dist/tokens/component/progress.d.ts +65 -5
  85. package/dist/tokens/component/progress.js +79 -18
  86. package/dist/tokens/semantic/BlendTokens.d.ts +10 -3
  87. package/dist/tokens/semantic/BlendTokens.js +17 -5
  88. package/dist/tokens/semantic/OpacityTokens.d.ts +4 -4
  89. package/dist/tokens/semantic/OpacityTokens.js +4 -4
  90. package/dist/types/ComponentTypes.d.ts +1 -1
  91. package/dist/types/generated/TokenTypes.d.ts +1 -1
  92. package/dist/types/generated/TokenTypes.js +1 -1
  93. package/dist/validators/StemmaTokenUsageValidator.js +3 -2
  94. package/dist/web/DesignTokens.web.css +1 -1
  95. package/governance/BUILD-SYSTEM-SETUP.md +1 -2
  96. package/governance/Component-Development-Guide.md +23 -13
  97. package/governance/Component-Development-Standards.md +21 -20
  98. package/governance/Component-Family-Avatar.md +6 -7
  99. package/governance/Component-Family-Badge.md +19 -20
  100. package/governance/Component-Family-Button.md +30 -43
  101. package/governance/Component-Family-Chip.md +14 -15
  102. package/governance/Component-Family-Container.md +12 -13
  103. package/governance/Component-Family-Data-Display.md +1 -2
  104. package/governance/Component-Family-Divider.md +1 -2
  105. package/governance/Component-Family-Form-Inputs.md +65 -65
  106. package/governance/Component-Family-Icon.md +9 -10
  107. package/governance/Component-Family-Loading.md +1 -2
  108. package/governance/Component-Family-Modal.md +1 -2
  109. package/governance/Component-Family-Navigation.md +1 -2
  110. package/governance/Component-Family-Progress.md +0 -1
  111. package/governance/Component-Inheritance-Structures.md +219 -99
  112. package/governance/Component-MCP-Document-Template.md +6 -5
  113. package/governance/Component-Primitive-vs-Semantic-Philosophy.md +1 -1
  114. package/governance/Component-Quick-Reference.md +33 -33
  115. package/governance/Component-Readiness-Status.md +59 -44
  116. package/governance/Component-Templates.md +57 -61
  117. package/governance/Contract-System-Reference.md +7 -7
  118. package/governance/MCP-Integration-Guide.md +1 -1
  119. package/governance/Process-Cross-Reference-Standards.md +31 -13
  120. package/governance/Process-Development-Workflow.md +49 -59
  121. package/governance/Process-File-Organization.md +25 -28
  122. package/governance/Process-Hook-Operations.md +22 -11
  123. package/governance/Process-Orchestration-Model-Selection.md +92 -0
  124. package/governance/Process-Spec-Planning.md +98 -52
  125. package/governance/Process-Task-Type-Definitions.md +80 -4
  126. package/governance/Product-Handoff-Protocol.md +2 -0
  127. package/governance/Rosetta-System-Architecture.md +13 -11
  128. package/governance/Test-Behavioral-Contract-Validation.md +38 -31
  129. package/governance/Test-Failure-Audit-Methodology.md +1 -1
  130. package/governance/Token-Family-Accessibility.md +1 -2
  131. package/governance/Token-Family-Blend.md +18 -16
  132. package/governance/Token-Family-Blur.md +0 -1
  133. package/governance/Token-Family-Border.md +1 -2
  134. package/governance/Token-Family-Color.md +0 -1
  135. package/governance/Token-Family-Glow.md +1 -2
  136. package/governance/Token-Family-Layering.md +0 -1
  137. package/governance/Token-Family-Motion.md +1 -2
  138. package/governance/Token-Family-Opacity.md +0 -1
  139. package/governance/Token-Family-Radius.md +1 -2
  140. package/governance/Token-Family-Responsive.md +1 -2
  141. package/governance/Token-Family-Shadow.md +1 -2
  142. package/governance/Token-Family-Sizing.md +0 -1
  143. package/governance/Token-Family-Spacing.md +1 -2
  144. package/governance/Token-Family-Typography.md +1 -2
  145. package/governance/Token-Governance.md +8 -8
  146. package/governance/Token-Quick-Reference.md +47 -34
  147. package/governance/Token-Resolution-Patterns.md +1 -1
  148. package/governance/Token-Semantic-Structure.md +1 -1
  149. package/governance/Web-Authoring-Standards.md +5 -5
  150. package/governance/browser-distribution-guide.md +1 -4
  151. package/governance/classification-map.md +474 -0
  152. package/governance/completion-documentation-guide.md +23 -37
  153. package/governance/component-meta-authoring-guide.md +1 -1
  154. package/governance/cross-platform-vs-platform-specific-decision-framework.md +1 -1
  155. package/governance/platform-implementation-guidelines.md +2 -3
  156. package/governance/release-management-system.md +28 -63
  157. package/governance/rosetta-system-principles.md +8 -6
  158. package/governance/stemma-system-principles.md +18 -17
  159. package/mcp-server/src/index.ts +24 -6
  160. package/mcp-server/src/indexer/DocumentIndexer.ts +119 -9
  161. package/mcp-server/src/indexer/__tests__/bare-id-crossrefs.test.ts +250 -0
  162. package/mcp-server/src/indexer/cross-ref-parser.ts +29 -1
  163. package/mcp-server/src/indexer/index-health.ts +27 -2
  164. package/mcp-server/src/query/__tests__/find-docs-calibration.test.ts +11 -26
  165. package/mcp-server/src/relocation-integrity-gate/__tests__/relocation-integrity-gate.test.ts +72 -5
  166. package/mcp-server/src/relocation-integrity-gate/relocation-integrity-gate.ts +81 -24
  167. package/mcp-server/src/tools/list-cross-references.ts +2 -2
  168. package/package.json +24 -26
  169. package/src/__tests__/browser-distribution/css-bundling.test.ts +6 -4
  170. package/src/__tests__/console-allowlist.json +14 -0
  171. package/src/__tests__/console-fail-setup.ts +169 -0
  172. package/src/__tests__/integration/Spec107-DesignLanguageContext.test.ts +16 -0
  173. package/src/__tests__/stemma-system/behavioral-contract-validation.test.ts +70 -17
  174. package/src/__tests__/stemma-system/contract-catalog-name-validation.test.ts +28 -0
  175. package/src/__tests__/stemma-system/form-inputs-contracts.test.ts +223 -16
  176. package/src/__tests__/stemma-system/input-text-native-base-call-alignment.test.ts +298 -0
  177. package/src/blend/OklchBlendCalculator.ts +3 -0
  178. package/src/blend/ThemeAwareBlendUtilities.android.kt +3 -0
  179. package/src/blend/ThemeAwareBlendUtilities.ios.swift +3 -0
  180. package/src/blend/ThemeAwareBlendUtilities.web.ts +9 -1
  181. package/src/blend/__tests__/InteractionStateAudit.test.ts +12 -9
  182. package/src/build/errors/__tests__/ErrorHandler.integration.test.ts +8 -0
  183. package/src/build/errors/__tests__/ErrorHandler.test.ts +5 -0
  184. package/src/build/tokens/__tests__/defineComponentTokens.test.ts +113 -0
  185. package/src/build/tokens/defineComponentTokens.ts +43 -1
  186. package/src/build/workflow/__tests__/CICDIntegration.test.ts +12 -1
  187. package/src/cli/__tests__/init.test.ts +45 -11
  188. package/src/components/core/Avatar-Base/Avatar-Base.schema.yaml +1 -1
  189. package/src/components/core/Avatar-Base/__tests__/Avatar.accessibility.test.ts +121 -7
  190. package/src/components/core/Avatar-Base/__tests__/Avatar.image.test.ts +3 -0
  191. package/src/components/core/Avatar-Base/__tests__/Avatar.test.ts +15 -6
  192. package/src/components/core/Avatar-Base/avatar.tokens.ts +31 -34
  193. package/src/components/core/Avatar-Base/contracts.yaml +11 -1
  194. package/src/components/core/Avatar-Base/index.ts +1 -1
  195. package/src/components/core/Avatar-Base/platforms/web/Avatar.web.ts +24 -5
  196. package/src/components/core/Badge-Count-Base/contracts.yaml +1 -1
  197. package/src/components/core/Badge-Label-Base/contracts.yaml +1 -1
  198. package/src/components/core/Button-CTA/Button-CTA.schema.yaml +2 -12
  199. package/src/components/core/Button-CTA/README.md +3 -6
  200. package/src/components/core/Button-CTA/__tests__/ButtonCTA.test.ts +35 -89
  201. package/src/components/core/Button-CTA/__tests__/setup.test.ts +0 -2
  202. package/src/components/core/Button-CTA/__tests__/test-utils.ts +0 -2
  203. package/src/components/core/Button-CTA/contracts.yaml +6 -29
  204. package/src/components/core/Button-CTA/examples/BasicUsage.html +2 -14
  205. package/src/components/core/Button-CTA/examples/BasicUsage.tsx +17 -44
  206. package/src/components/core/Button-CTA/platforms/android/ButtonCTA.android.kt +12 -20
  207. package/src/components/core/Button-CTA/platforms/ios/ButtonCTA.ios.swift +12 -51
  208. package/src/components/core/Button-CTA/platforms/web/ButtonCTA.web.css +2 -26
  209. package/src/components/core/Button-CTA/platforms/web/ButtonCTA.web.ts +18 -71
  210. package/src/components/core/Button-CTA/types.ts +10 -28
  211. package/src/components/core/Button-Icon/buttonIcon.tokens.ts +43 -27
  212. package/src/components/core/Chip-Base/__tests__/ChipBase.test.ts +13 -0
  213. package/src/components/core/Chip-Filter/__tests__/ChipFilter.test.ts +13 -0
  214. package/src/components/core/Chip-Input/__tests__/ChipInput.test.ts +13 -0
  215. package/src/components/core/Input-Text-Base/Input-Text-Base.schema.yaml +30 -2
  216. package/src/components/core/Input-Text-Base/README.md +25 -2
  217. package/src/components/core/Input-Text-Base/__tests__/focusIndicators.test.ts +16 -15
  218. package/src/components/core/Input-Text-Base/contracts.yaml +90 -0
  219. package/src/components/core/Input-Text-Base/platforms/android/InputTextBase.android.kt +26 -12
  220. package/src/components/core/Input-Text-Base/platforms/ios/InputTextBase.ios.swift +195 -59
  221. package/src/components/core/Input-Text-Base/types.ts +13 -1
  222. package/src/components/core/Input-Text-Email/Input-Text-Email.schema.yaml +5 -1
  223. package/src/components/core/Input-Text-Email/README.md +8 -7
  224. package/src/components/core/Input-Text-Email/platforms/android/InputTextEmail.android.kt +1 -4
  225. package/src/components/core/Input-Text-Email/platforms/ios/InputTextEmail.ios.swift +2 -16
  226. package/src/components/core/Input-Text-Password/Input-Text-Password.schema.yaml +10 -3
  227. package/src/components/core/Input-Text-Password/README.md +9 -8
  228. package/src/components/core/Input-Text-Password/contracts.yaml +5 -0
  229. package/src/components/core/Input-Text-Password/platforms/android/InputTextPassword.android.kt +17 -7
  230. package/src/components/core/Input-Text-Password/platforms/ios/InputTextPassword.ios.swift +22 -20
  231. package/src/components/core/Input-Text-Password/platforms/web/InputTextPassword.web.ts +11 -2
  232. package/src/components/core/Input-Text-PhoneNumber/Input-Text-PhoneNumber.schema.yaml +5 -1
  233. package/src/components/core/Input-Text-PhoneNumber/README.md +9 -8
  234. package/src/components/core/Input-Text-PhoneNumber/platforms/android/InputTextPhoneNumber.android.kt +2 -5
  235. package/src/components/core/Input-Text-PhoneNumber/platforms/ios/InputTextPhoneNumber.ios.swift +3 -17
  236. package/src/components/core/Nav-Header-App/contracts.yaml +1 -1
  237. package/src/components/core/Nav-SegmentedChoice-Base/contracts.yaml +1 -1
  238. package/src/components/core/Progress-Indicator-Connector-Base/contracts.yaml +1 -1
  239. package/src/components/core/Progress-Indicator-Label-Base/contracts.yaml +1 -1
  240. package/src/components/core/Progress-Indicator-Node-Base/contracts.yaml +1 -1
  241. package/src/components/core/Progress-Stepper-Base/__tests__/StepperBase.test.ts +5 -2
  242. package/src/components/core/Progress-Stepper-Detailed/__tests__/StepperDetailed.test.ts +5 -2
  243. package/src/generators/DTCGFormatGenerator.ts +6 -0
  244. package/src/generators/TokenFileGenerator.ts +7 -2
  245. package/src/generators/__tests__/DTCGConfigOptions.test.ts +14 -5
  246. package/src/integration/BuildErrorHandler.ts +2 -2
  247. package/src/tokens/OpacityTokens.ts +1 -1
  248. package/src/tokens/__tests__/OpacityTokens.test.ts +3 -1
  249. package/src/tokens/__tests__/ProgressTokenCompliance.test.ts +5 -3
  250. package/src/tokens/__tests__/ProgressTokenFormulas.test.ts +11 -11
  251. package/src/tokens/__tests__/ProgressTokenTranslation.test.ts +22 -20
  252. package/src/tokens/component/progress.ts +83 -21
  253. package/src/tokens/semantic/BlendTokens.ts +26 -5
  254. package/src/tokens/semantic/OpacityTokens.ts +4 -4
  255. package/src/types/ComponentTypes.ts +1 -1
  256. package/src/types/generated/TokenTypes.ts +1 -1
  257. package/src/validators/StemmaTokenUsageValidator.ts +3 -2
  258. package/token-index/components.yaml +8 -8
  259. package/token-index/semantics.yaml +1 -2
  260. package/src/tools/release/__tests__/ChangeClassifier.test.ts +0 -133
  261. package/src/tools/release/__tests__/ChangeExtractor.test.ts +0 -222
  262. package/src/tools/release/__tests__/GitHubPublisher.test.ts +0 -240
  263. package/src/tools/release/__tests__/NotesRenderer.test.ts +0 -142
  264. package/src/tools/release/__tests__/NpmPublisher.test.ts +0 -289
  265. package/src/tools/release/__tests__/PipelineIntegration.test.ts +0 -188
  266. package/src/tools/release/__tests__/ReleasePipeline.test.ts +0 -192
  267. package/src/tools/release/__tests__/SemanticVersionValidator.test.ts +0 -49
  268. package/src/tools/release/__tests__/SummaryScanner.test.ts +0 -141
  269. package/src/tools/release/__tests__/TagResolver.test.ts +0 -91
  270. package/src/tools/release/__tests__/VersionCalculator.test.ts +0 -270
  271. package/src/tools/release/__tests__/helpers/NpmMockHelper.ts +0 -80
  272. package/src/tools/release/cli/ReleasePipeline.ts +0 -165
  273. package/src/tools/release/cli/release-tool.ts +0 -107
  274. package/src/tools/release/pipeline/ChangeClassifier.ts +0 -61
  275. package/src/tools/release/pipeline/ChangeExtractor.ts +0 -87
  276. package/src/tools/release/pipeline/NotesRenderer.ts +0 -66
  277. package/src/tools/release/pipeline/SummaryScanner.ts +0 -70
  278. package/src/tools/release/pipeline/TagResolver.ts +0 -40
  279. package/src/tools/release/pipeline/VersionCalculator.ts +0 -375
  280. package/src/tools/release/publishers/GitHubPublisher.ts +0 -228
  281. package/src/tools/release/publishers/NpmPublisher.ts +0 -196
  282. package/src/tools/release/release-config.json +0 -5
  283. package/src/tools/release/types/index.ts +0 -282
  284. package/src/tools/release/validators/SemanticVersionValidator.ts +0 -67
@@ -4,7 +4,7 @@ import { extractMetadata } from './metadata-parser';
4
4
  import { extractFrontmatterInfo } from './frontmatter-parser';
5
5
  import { extractHeadingStructure } from './heading-parser';
6
6
  import { resolveSection, SectionLookup } from './section-parser';
7
- import { extractCrossReferences } from './cross-ref-parser';
7
+ import { extractCrossReferences, CrossReference as ParsedCrossReference } from './cross-ref-parser';
8
8
  import { estimateTokenCount } from '../utils/token-estimator';
9
9
  import { determineIndexHealth } from './index-health';
10
10
  import { seedLegacyPathsFromFrozenManifest } from '../legacy-path';
@@ -65,6 +65,25 @@ export class DocumentIndexer {
65
65
  private idIndex: Map<string, string> = new Map(); // id → indexedKey
66
66
  private legacyPathIndex: Map<string, string> = new Map(); // normalizedLegacyPath → indexedKey
67
67
 
68
+ /**
69
+ * Validated cross-references per indexed key (Spec 119-B OB-1, Decision 1:
70
+ * extract-then-validate at INDEX time, one validation point). indexFile
71
+ * stores the raw parser output (bare-id candidates still tagged); the
72
+ * post-index validation pass (or reindexFile's inline pass) replaces it with
73
+ * the validated, TAG-STRIPPED set — the candidate tag never escapes the
74
+ * indexer. All read surfaces (list_cross_references, getDocumentSummary,
75
+ * index-health metrics) consume THIS map, never re-extract, so the
76
+ * crossReferences count is a stable index property.
77
+ */
78
+ private crossRefsByKey: Map<string, ParsedCrossReference[]> = new Map();
79
+ /**
80
+ * Bare-id candidates dropped by validation (target not in idIndex) — the
81
+ * typo-suspicious class. Surfaced on two channels (design Component 6):
82
+ * scan-cross-references.sh lists individually; index-health emits ONE
83
+ * aggregate warning when count > 0.
84
+ */
85
+ private droppedIdCandidates: Array<{ sourceKey: string; target: string; section: string; lineNumber: number }> = [];
86
+
68
87
  private lastIndexTime: string | undefined;
69
88
  private directoryPath: string | undefined;
70
89
  private logsDirectory: string;
@@ -104,6 +123,8 @@ export class DocumentIndexer {
104
123
  this.documentContent.clear();
105
124
  this.idIndex.clear();
106
125
  this.legacyPathIndex.clear();
126
+ this.crossRefsByKey.clear();
127
+ this.droppedIdCandidates = [];
107
128
  // RE-SEED OBLIGATION (Task 3.2): legacyPathIndex is seeded out-of-band from a
108
129
  // build-time manifest via loadLegacyPathManifest — it is NOT derived from
109
130
  // on-disk scanning (the original `.kiro/steering/…` paths no longer exist
@@ -131,6 +152,13 @@ export class DocumentIndexer {
131
152
  // no legacy fallback (correct degraded behavior).
132
153
  this.seedLegacyPaths();
133
154
 
155
+ // CROSS-REF VALIDATION PASS (Spec 119-B OB-1): joins the same post-index
156
+ // hook as the legacy re-seed — idIndex is complete here, so every bare-id
157
+ // candidate can be validated in ONE place (Decision 1: index-time, never
158
+ // query-time). Hits become real cross-references (target already the doc
159
+ // id); misses are dropped and recorded for the two surfacing channels.
160
+ this.validateAllCrossReferences();
161
+
134
162
  // Update last index time
135
163
  this.lastIndexTime = new Date().toISOString();
136
164
 
@@ -157,12 +185,20 @@ export class DocumentIndexer {
157
185
  this.pruneAddressingEntriesForKey(filePath);
158
186
  this.documentMap.delete(filePath);
159
187
  this.documentContent.delete(filePath);
188
+ this.crossRefsByKey.delete(filePath);
189
+ this.droppedIdCandidates = this.droppedIdCandidates.filter(d => d.sourceKey !== filePath);
160
190
  this.lastIndexTime = new Date().toISOString();
161
191
  return;
162
192
  }
163
193
 
164
194
  // Re-index the file (the re-add branch repopulates idIndex via indexFile).
165
195
  await this.indexFile(filePath);
196
+ // Inline cross-ref validation against the STANDING idIndex (Spec 119-B
197
+ // OB-1) — same validation function as the full-rebuild pass. ACCEPTED EDGE
198
+ // (pinned by test): a ref to a NEW doc B not yet in the standing idIndex is
199
+ // dropped until the next full rebuild — covered operationally by the
200
+ // write-side rebuild protocol.
201
+ this.validateCrossReferencesForKey(filePath);
166
202
  this.lastIndexTime = new Date().toISOString();
167
203
  }
168
204
 
@@ -206,14 +242,16 @@ export class DocumentIndexer {
206
242
  * @param filePath - Path to document
207
243
  */
208
244
  getDocumentSummary(filePath: string): DocumentSummary {
209
- const content = this.getDocumentContent(filePath);
245
+ const { indexedKey } = this.resolveRef(filePath);
246
+ const content = this.documentContent.get(indexedKey)!;
210
247
  const metadata = extractMetadata(content);
211
248
 
212
249
  // Extract heading structure
213
250
  const outline = extractHeadingStructure(content);
214
251
 
215
- // Extract cross-references
216
- const crossReferences = extractCrossReferences(content, filePath);
252
+ // Cross-references: the VALIDATED index-time set (Spec 119-B OB-1) — same
253
+ // single source list_cross_references serves; dropped candidates excluded.
254
+ const crossReferences = this.crossRefsByKey.get(indexedKey) ?? [];
217
255
 
218
256
  // Convert CrossReference[] to CrossReferenceInfo[]
219
257
  const crossReferenceInfo = crossReferences.map(ref => ({
@@ -350,12 +388,71 @@ export class DocumentIndexer {
350
388
  /**
351
389
  * List cross-references in a document
352
390
  * Returns links without following them
353
- *
354
- * @param filePath - Path to document
391
+ *
392
+ * Spec 119-B OB-1: the incoming ref routes through the SAME resolver chain
393
+ * as every other document-addressed tool (id → indexed key → legacy path,
394
+ * D5), and the result is the VALIDATED index-time set — `.md` path refs
395
+ * unchanged, bare-id refs enumerated with target = the doc id; dropped
396
+ * candidates never appear. Public shape unchanged (no kind tag).
397
+ *
398
+ * @param filePath - Document ref: id, indexed relative path, or legacy path
355
399
  */
356
400
  listCrossReferences(filePath: string): CrossReference[] {
357
- const content = this.getDocumentContent(filePath);
358
- return extractCrossReferences(content, filePath);
401
+ const { indexedKey } = this.resolveRef(filePath);
402
+ return this.crossRefsByKey.get(indexedKey) ?? [];
403
+ }
404
+
405
+ /**
406
+ * Bare-id candidates dropped by validation (Spec 119-B OB-1) — the
407
+ * typo-suspicious class surfaced via index-health's aggregate warning and
408
+ * scan-cross-references.sh's individual listing.
409
+ */
410
+ getDroppedIdCandidates(): ReadonlyArray<{ sourceKey: string; target: string; section: string; lineNumber: number }> {
411
+ return this.droppedIdCandidates;
412
+ }
413
+
414
+ /**
415
+ * Validate ALL stored cross-reference sets against the completed idIndex
416
+ * (the post-index hook — Spec 119-B OB-1, Decision 1's single validation
417
+ * point). Resets the dropped-candidate record.
418
+ */
419
+ private validateAllCrossReferences(): void {
420
+ this.droppedIdCandidates = [];
421
+ for (const key of this.crossRefsByKey.keys()) {
422
+ this.validateCrossReferencesForKey(key);
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Validate one document's stored cross-references against the CURRENT
428
+ * idIndex. `.md` path refs pass through untouched; bare-id candidates are
429
+ * kept iff their target is a known doc id (tag stripped — it never escapes
430
+ * the indexer) and dropped-with-record otherwise.
431
+ */
432
+ private validateCrossReferencesForKey(key: string): void {
433
+ const raw = this.crossRefsByKey.get(key);
434
+ if (!raw) return;
435
+
436
+ // Replace any prior dropped entries for this key (reindex path).
437
+ this.droppedIdCandidates = this.droppedIdCandidates.filter(d => d.sourceKey !== key);
438
+
439
+ const validated: ParsedCrossReference[] = [];
440
+ for (const ref of raw) {
441
+ if (ref.kind !== 'id-candidate') {
442
+ validated.push(ref);
443
+ } else if (this.idIndex.has(ref.target)) {
444
+ const { kind: _kind, ...publicRef } = ref;
445
+ validated.push(publicRef);
446
+ } else {
447
+ this.droppedIdCandidates.push({
448
+ sourceKey: key,
449
+ target: ref.target,
450
+ section: ref.section,
451
+ lineNumber: ref.lineNumber,
452
+ });
453
+ }
454
+ }
455
+ this.crossRefsByKey.set(key, validated);
359
456
  }
360
457
 
361
458
  /**
@@ -483,6 +580,13 @@ export class DocumentIndexer {
483
580
 
484
581
  this.documentMap.set(filePath, documentMetadata);
485
582
 
583
+ // Store the RAW parser output (bare-id candidates still tagged) for the
584
+ // post-index validation pass (Spec 119-B OB-1). Mid-indexing this map may
585
+ // hold unvalidated candidates — accepted and explicit (design Component 6):
586
+ // no consumer reads cross-refs during indexing; indexing is atomic from the
587
+ // API's view.
588
+ this.crossRefsByKey.set(filePath, extractCrossReferences(content, filePath));
589
+
486
590
  // Build the id index (id → indexed key). First drop any PRIOR id forward-entry
487
591
  // that still points at this same key — covers an in-place id rewrite (reindexFile
488
592
  // re-add branch with NO delete event), which would otherwise leave a stale
@@ -690,10 +794,16 @@ export class DocumentIndexer {
690
794
  }
691
795
 
692
796
  // Use determineIndexHealth for comprehensive health check
797
+ let validatedCount = 0;
798
+ for (const refs of this.crossRefsByKey.values()) validatedCount += refs.length;
693
799
  const health = determineIndexHealth({
694
800
  indexedDocuments: this.documentContent,
695
801
  directoryPath: this.directoryPath,
696
- lastIndexTime: this.lastIndexTime
802
+ lastIndexTime: this.lastIndexTime,
803
+ crossRefTotals: {
804
+ validatedCount,
805
+ droppedBareIdCount: this.droppedIdCandidates.length
806
+ }
697
807
  });
698
808
 
699
809
  this.logIndexStateChange('validation_completed', {
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Bare-id cross-reference enumeration — unit tests (Spec 119-B OB-1, R9,
3
+ * design Component 6).
4
+ *
5
+ * Covers: the parser's bare-id candidate grammar (positives + false-positive
6
+ * guards), `.md` extraction unchanged (no tag on path refs), the indexer's
7
+ * post-index validation pass (hits kept tag-stripped, misses dropped with
8
+ * record), the migrated-doc enumeration case (token-governance-pattern
9
+ * fixture), the reindexFile ACCEPTED EDGE (new-doc ref dropped until full
10
+ * rebuild), the D5 addressing contract on listCrossReferences, and the
11
+ * index-health aggregate warning + stable validated count.
12
+ */
13
+
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+ import { extractCrossReferences, BARE_ID_GRAMMAR } from '../cross-ref-parser';
17
+ import { DocumentIndexer } from '../DocumentIndexer';
18
+
19
+ const TEST_FIXTURES_DIR = path.join(__dirname, 'fixtures');
20
+
21
+ function docWithId(id: string, h1: string, body: string): string {
22
+ return `---
23
+ name: ${h1}
24
+ id: ${id}
25
+ description: fixture doc
26
+ ---
27
+
28
+ # ${h1}
29
+
30
+ **Date**: 2026-08-02
31
+ **Purpose**: OB-1 fixture
32
+ **Organization**: test-org
33
+ **Scope**: test
34
+ **Layer**: 2
35
+ **Relevant Tasks**: testing
36
+ **Last Reviewed**: 2026-08-02
37
+
38
+ ## Body
39
+
40
+ ${body}
41
+ `;
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Parser: grammar + guards (7.2)
46
+ // ---------------------------------------------------------------------------
47
+
48
+ describe('cross-ref parser — bare-id candidate extraction (Spec 119-B OB-1)', () => {
49
+ it('grammar positives: extracts bare-id candidates with the internal tag', () => {
50
+ const content = '## S\n\nsee [Token Governance](token-governance) and [x](a1) and [y](x-y-z2).';
51
+ const refs = extractCrossReferences(content, 'test.md');
52
+ expect(refs).toHaveLength(3);
53
+ expect(refs.map(r => r.target)).toEqual(['token-governance', 'a1', 'x-y-z2']);
54
+ for (const r of refs) expect(r.kind).toBe('id-candidate');
55
+ });
56
+
57
+ it('false-positive guards: anchors, URLs, paths, dotted names, uppercase, underscores, code-ish targets are NOT candidates', () => {
58
+ const content = [
59
+ '## S',
60
+ '[anchor](#section)',
61
+ '[url](https://example.com/page)',
62
+ '[url2](mailto:x@y.z)',
63
+ '[relpath](./sub/thing)',
64
+ '[abspath](/root/thing)',
65
+ '[dotted](file.txt)',
66
+ '[upper](Not-An-Id)',
67
+ '[underscore](not_an_id)',
68
+ '[hyphen-start](-bad)',
69
+ '[empty]()',
70
+ ].join('\n');
71
+ const refs = extractCrossReferences(content, 'test.md');
72
+ expect(refs).toHaveLength(0);
73
+ });
74
+
75
+ it('.md extraction is unchanged: path refs carry NO kind tag and keep their exact shape', () => {
76
+ const content = '## Section One\n\nsee [Guide](./guide.md) and [Other](docs/other.md#anchor).';
77
+ const refs = extractCrossReferences(content, 'test.md');
78
+ expect(refs).toEqual([
79
+ { target: './guide.md', context: 'Guide', section: 'Section One', lineNumber: 3 },
80
+ { target: 'docs/other.md#anchor', context: 'Other', section: 'Section One', lineNumber: 3 },
81
+ ]);
82
+ });
83
+
84
+ it('mixed line: .md ref and bare-id candidate extracted side by side', () => {
85
+ const content = '## S\n\n[A](a.md) then [B](token-governance) then [C](#x).';
86
+ const refs = extractCrossReferences(content, 'test.md');
87
+ expect(refs.map(r => [r.target, r.kind ?? 'path'])).toEqual([
88
+ ['a.md', 'path'],
89
+ ['token-governance', 'id-candidate'],
90
+ ]);
91
+ });
92
+
93
+ it('grammar constant rejects / . : # by construction', () => {
94
+ for (const bad of ['a/b', 'a.b', 'a:b', 'a#b', 'A', '-a', '']) {
95
+ expect(BARE_ID_GRAMMAR.test(bad) && !/[/.:#]/.test(bad)).toBe(false);
96
+ }
97
+ });
98
+ });
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Indexer: validation pass + surfacing + D5 (7.3 / 7.4)
102
+ // ---------------------------------------------------------------------------
103
+
104
+ describe('DocumentIndexer — bare-id validation on the post-index hook (Spec 119-B OB-1)', () => {
105
+ let indexer: DocumentIndexer;
106
+ let testDir: string;
107
+
108
+ beforeEach(() => {
109
+ indexer = new DocumentIndexer();
110
+ testDir = path.join(TEST_FIXTURES_DIR, `bareid-${Date.now()}-${Math.random().toString(36).slice(2)}`);
111
+ fs.mkdirSync(testDir, { recursive: true });
112
+ });
113
+
114
+ afterEach(() => {
115
+ if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
116
+ });
117
+
118
+ it('validates candidates against idIndex: hits kept (tag stripped), misses dropped with record', async () => {
119
+ fs.writeFileSync(
120
+ path.join(testDir, 'doc-a.md'),
121
+ docWithId('doc-a', 'Doc A', 'see [B](doc-b), [bogus](no-such-id), and [file](other.md).')
122
+ );
123
+ fs.writeFileSync(path.join(testDir, 'doc-b.md'), docWithId('doc-b', 'Doc B', 'plain body.'));
124
+ await indexer.indexDirectory(testDir);
125
+
126
+ const refs = indexer.listCrossReferences('doc-a');
127
+ expect(refs.map(r => r.target).sort()).toEqual(['doc-b', 'other.md']);
128
+ // The candidate tag never escapes the indexer (public shape unchanged)
129
+ for (const r of refs) expect('kind' in r).toBe(false);
130
+
131
+ const dropped = indexer.getDroppedIdCandidates();
132
+ expect(dropped).toHaveLength(1);
133
+ expect(dropped[0].target).toBe('no-such-id');
134
+ expect(dropped[0].sourceKey).toContain('doc-a.md');
135
+ expect(dropped[0].lineNumber).toBeGreaterThan(0);
136
+ });
137
+
138
+ it('migrated-doc enumeration: a token-governance-pattern doc has its bare-id refs enumerated', async () => {
139
+ // Mirrors the real post-U3 pattern: prose linking sibling docs by bare id
140
+ const body = [
141
+ 'Token selection: see [Token Quick Reference](token-quick-reference) for patterns,',
142
+ '[Rosetta System Architecture](rosetta-system-architecture) for the pipeline,',
143
+ 'and the [DTCG guide](dtcg-integration-guide) for exports.',
144
+ ].join('\n');
145
+ fs.writeFileSync(path.join(testDir, 'token-governance.md'), docWithId('token-governance', 'Token Governance', body));
146
+ fs.writeFileSync(path.join(testDir, 'tqr.md'), docWithId('token-quick-reference', 'Token Quick Reference', 'x'));
147
+ fs.writeFileSync(path.join(testDir, 'rsa.md'), docWithId('rosetta-system-architecture', 'Rosetta System Architecture', 'x'));
148
+ fs.writeFileSync(path.join(testDir, 'dtcg.md'), docWithId('dtcg-integration-guide', 'DTCG Integration Guide', 'x'));
149
+ await indexer.indexDirectory(testDir);
150
+
151
+ const refs = indexer.listCrossReferences('token-governance');
152
+ expect(refs.map(r => r.target).sort()).toEqual([
153
+ 'dtcg-integration-guide',
154
+ 'rosetta-system-architecture',
155
+ 'token-quick-reference',
156
+ ]);
157
+ expect(indexer.getDroppedIdCandidates()).toHaveLength(0);
158
+ // Context + section + line survive for bare-id refs (same fields as .md refs)
159
+ expect(refs[0].section).toBe('Body');
160
+ expect(refs[0].context.length).toBeGreaterThan(0);
161
+ });
162
+
163
+ it('ACCEPTED EDGE (pinned): reindexFile drops a ref to a NEW doc not yet in the standing idIndex; the next full rebuild restores it', async () => {
164
+ const aPath = path.join(testDir, 'doc-a.md');
165
+ fs.writeFileSync(aPath, docWithId('doc-a', 'Doc A', 'no links yet.'));
166
+ await indexer.indexDirectory(testDir);
167
+
168
+ // NEW doc B lands on disk; doc A is edited to reference it; ONLY A is reindexed.
169
+ fs.writeFileSync(path.join(testDir, 'doc-b.md'), docWithId('doc-b', 'Doc B', 'new doc.'));
170
+ fs.writeFileSync(aPath, docWithId('doc-a', 'Doc A', 'now see [B](doc-b).'));
171
+ await indexer.reindexFile(aPath);
172
+
173
+ // B is not in the standing idIndex (it was never indexed) → ref dropped
174
+ expect(indexer.listCrossReferences('doc-a').map(r => r.target)).toEqual([]);
175
+ expect(indexer.getDroppedIdCandidates().map(d => d.target)).toEqual(['doc-b']);
176
+
177
+ // Full rebuild: idIndex complete → the ref validates
178
+ await indexer.indexDirectory(testDir);
179
+ expect(indexer.listCrossReferences('doc-a').map(r => r.target)).toEqual(['doc-b']);
180
+ expect(indexer.getDroppedIdCandidates()).toHaveLength(0);
181
+ });
182
+
183
+ it('reindexFile validates inline when the target IS in the standing idIndex', async () => {
184
+ const aPath = path.join(testDir, 'doc-a.md');
185
+ fs.writeFileSync(aPath, docWithId('doc-a', 'Doc A', 'no links yet.'));
186
+ fs.writeFileSync(path.join(testDir, 'doc-b.md'), docWithId('doc-b', 'Doc B', 'x'));
187
+ await indexer.indexDirectory(testDir);
188
+
189
+ fs.writeFileSync(aPath, docWithId('doc-a', 'Doc A', 'now see [B](doc-b).'));
190
+ await indexer.reindexFile(aPath);
191
+ expect(indexer.listCrossReferences('doc-a').map(r => r.target)).toEqual(['doc-b']);
192
+ });
193
+
194
+ it('D5: listCrossReferences resolves via id, indexed key, and ./-prefixed key through the shared resolver chain', async () => {
195
+ const aPath = path.join(testDir, 'doc-a.md');
196
+ fs.writeFileSync(aPath, docWithId('doc-a', 'Doc A', 'see [B](doc-b) and [G](./guide.md).'));
197
+ fs.writeFileSync(path.join(testDir, 'doc-b.md'), docWithId('doc-b', 'Doc B', 'x'));
198
+ await indexer.indexDirectory(testDir);
199
+
200
+ const byId = indexer.listCrossReferences('doc-a');
201
+ const byKey = indexer.listCrossReferences(aPath);
202
+ const byDotSlash = indexer.listCrossReferences(`./${aPath}`);
203
+ expect(byKey).toEqual(byId);
204
+ expect(byDotSlash).toEqual(byId);
205
+ expect(byId.map(r => r.target).sort()).toEqual(['./guide.md', 'doc-b']);
206
+ });
207
+
208
+ it('D5 miss: an unresolvable ref throws DocumentNotResolved (same contract as the other document tools)', async () => {
209
+ fs.writeFileSync(path.join(testDir, 'doc-a.md'), docWithId('doc-a', 'Doc A', 'x'));
210
+ await indexer.indexDirectory(testDir);
211
+ expect(() => indexer.listCrossReferences('nope-never')).toThrow(/Document not found/);
212
+ });
213
+
214
+ it('getDocumentSummary serves the same validated set (dropped candidates never appear)', async () => {
215
+ fs.writeFileSync(
216
+ path.join(testDir, 'doc-a.md'),
217
+ docWithId('doc-a', 'Doc A', 'see [B](doc-b) and [bogus](no-such-id).')
218
+ );
219
+ fs.writeFileSync(path.join(testDir, 'doc-b.md'), docWithId('doc-b', 'Doc B', 'x'));
220
+ await indexer.indexDirectory(testDir);
221
+
222
+ const summary = indexer.getDocumentSummary('doc-a');
223
+ expect(summary.crossReferences.map(r => r.target)).toEqual(['doc-b']);
224
+ });
225
+
226
+ it('index-health: totalCrossReferences is the validated count and drops emit ONE aggregate warning', async () => {
227
+ fs.writeFileSync(
228
+ path.join(testDir, 'doc-a.md'),
229
+ docWithId('doc-a', 'Doc A', 'see [B](doc-b), [bogus](no-such-id), and [file](guide.md).')
230
+ );
231
+ fs.writeFileSync(path.join(testDir, 'doc-b.md'), docWithId('doc-b', 'Doc B', 'x'));
232
+ await indexer.indexDirectory(testDir);
233
+
234
+ const health = indexer.getIndexHealth();
235
+ // validated = doc-b (bare-id hit) + guide.md (path ref) = 2; bogus dropped
236
+ expect(health.metrics.totalCrossReferences).toBe(2);
237
+ const warning = health.warnings.filter(w => w.includes('unresolved bare-id'));
238
+ expect(warning).toHaveLength(1);
239
+ expect(warning[0]).toBe('1 unresolved bare-id link targets — run scan-cross-references.sh for the list');
240
+ });
241
+
242
+ it('index-health: zero drops emits NO bare-id warning', async () => {
243
+ fs.writeFileSync(path.join(testDir, 'doc-a.md'), docWithId('doc-a', 'Doc A', 'see [B](doc-b).'));
244
+ fs.writeFileSync(path.join(testDir, 'doc-b.md'), docWithId('doc-b', 'Doc B', 'x'));
245
+ await indexer.indexDirectory(testDir);
246
+
247
+ const health = indexer.getIndexHealth();
248
+ expect(health.warnings.filter(w => w.includes('unresolved bare-id'))).toHaveLength(0);
249
+ });
250
+ });
@@ -8,12 +8,28 @@
8
8
  */
9
9
 
10
10
  export interface CrossReference {
11
- target: string; // Referenced document path
11
+ target: string; // Referenced document path (or doc id, for bare-id refs)
12
12
  context: string; // Context description from link text
13
13
  section: string; // Source section containing reference
14
14
  lineNumber: number; // Line number in source file
15
+ /**
16
+ * INTERNAL-ONLY candidate tag (Spec 119-B OB-1). Present ONLY on bare-id
17
+ * candidates awaiting idIndex validation; `.md` path refs never carry it
18
+ * (their extracted shape is unchanged). The tag never escapes the indexer:
19
+ * validation strips it before refs reach any public surface.
20
+ */
21
+ kind?: 'id-candidate';
15
22
  }
16
23
 
24
+ /**
25
+ * Bare-id candidate grammar (Spec 119-B OB-1 / design Component 6): a link
26
+ * target is an id candidate IF it matches this AND contains none of `/ . : #`
27
+ * (the character class already excludes them; the explicit guard in the
28
+ * extractor documents the contract). Validation against idIndex happens in the
29
+ * indexer's post-index pass — the parser stays a dumb extractor.
30
+ */
31
+ export const BARE_ID_GRAMMAR = /^[a-z0-9][a-z0-9-]*$/;
32
+
17
33
  /**
18
34
  * Extract cross-references from markdown content
19
35
  *
@@ -57,6 +73,18 @@ export function extractCrossReferences(content: string, _filePath: string): Cros
57
73
  section: currentSection,
58
74
  lineNumber: i + 1
59
75
  });
76
+ } else if (BARE_ID_GRAMMAR.test(target) && !/[/.:#]/.test(target)) {
77
+ // Bare-id candidate (Spec 119-B OB-1): tagged, NOT validated here —
78
+ // anchors (#…), URLs (contain :/), and paths (contain / or .) never
79
+ // reach this branch. The indexer's post-index pass validates against
80
+ // idIndex and drops misses.
81
+ references.push({
82
+ target,
83
+ context,
84
+ section: currentSection,
85
+ lineNumber: i + 1,
86
+ kind: 'id-candidate'
87
+ });
60
88
  }
61
89
  }
62
90
  }
@@ -24,6 +24,17 @@ export interface HealthCheckOptions {
24
24
  directoryPath: string;
25
25
  /** Last index time (ISO string) */
26
26
  lastIndexTime?: string;
27
+ /**
28
+ * Validated cross-reference totals from the indexer (Spec 119-B OB-1).
29
+ * When provided, `totalCrossReferences` reports the VALIDATED index-time
30
+ * count (a stable index property — Decision 1) instead of a re-extraction,
31
+ * and `droppedBareIdCount > 0` emits ONE aggregate warning pointing at
32
+ * scan-cross-references.sh for the individual listing.
33
+ */
34
+ crossRefTotals?: {
35
+ validatedCount: number;
36
+ droppedBareIdCount: number;
37
+ };
27
38
  }
28
39
 
29
40
  /**
@@ -38,7 +49,7 @@ export interface HealthCheckOptions {
38
49
  * @returns IndexHealth with status, errors, warnings, and metrics
39
50
  */
40
51
  export function determineIndexHealth(options: HealthCheckOptions): IndexHealth {
41
- const { indexedDocuments, directoryPath, lastIndexTime } = options;
52
+ const { indexedDocuments, directoryPath, lastIndexTime, crossRefTotals } = options;
42
53
 
43
54
  const errors: string[] = [];
44
55
  const warnings: string[] = [];
@@ -68,9 +79,23 @@ export function determineIndexHealth(options: HealthCheckOptions): IndexHealth {
68
79
  warnings.push(`Malformed metadata: ${malformedDocs.join(', ')}`);
69
80
  }
70
81
 
82
+ // Dropped bare-id candidates: ONE aggregate warning on the daily-consumer
83
+ // channel (Spec 119-B OB-1, design Component 6) — per-item detail lives in
84
+ // the scanner, not here.
85
+ if (crossRefTotals && crossRefTotals.droppedBareIdCount > 0) {
86
+ warnings.push(
87
+ `${crossRefTotals.droppedBareIdCount} unresolved bare-id link targets — run scan-cross-references.sh for the list`
88
+ );
89
+ }
90
+
71
91
  // Calculate metrics
72
92
  const metrics = calculateIndexMetrics(indexedDocuments);
73
-
93
+ // Spec 119-B OB-1: prefer the indexer's validated count (stable index
94
+ // property) over re-extraction when supplied.
95
+ if (crossRefTotals) {
96
+ metrics.totalCrossReferences = crossRefTotals.validatedCount;
97
+ }
98
+
74
99
  // Determine status
75
100
  let status: IndexHealthStatus;
76
101
  if (errors.length > 0) {
@@ -28,9 +28,7 @@
28
28
  * | concept query | must include | expected tier |
29
29
  * |------------------------|----------------------------------------|---------------|
30
30
  * | "RTL" | Web-Authoring-Standards.md | strong |
31
- * | | Component-Family-Form-Inputs.md | strong |
32
31
  * | "internationalization" | Web-Authoring-Standards.md | strong |
33
- * | | Component-Family-Form-Inputs.md | strong |
34
32
  * | "spec planning" | Process-Spec-Planning.md | strong |
35
33
  * | "EARS" | Process-Spec-Planning.md | strong |
36
34
  * | incidental-token case | only incidental match | partial |
@@ -60,7 +58,6 @@ const STEERING_DIR = path.resolve(__dirname, '../../../../governance');
60
58
  * absolute-path prefixes do not make the fixture brittle to CWD differences).
61
59
  */
62
60
  const RTL_DOC_A = 'Web-Authoring-Standards.md';
63
- const RTL_DOC_B = 'Component-Family-Form-Inputs.md';
64
61
  const SPEC_PLANNING_DOC = 'Process-Spec-Planning.md';
65
62
 
66
63
  // ---------------------------------------------------------------------------
@@ -132,7 +129,16 @@ describe('find_docs calibration fixtures — real corpus, tool boundary (Task 4.
132
129
  // Fixture 1 — RTL / right-to-left
133
130
  // -------------------------------------------------------------------------
134
131
 
135
- describe('fixture: "RTL" → Web-Authoring-Standards.md + Component-Family-Form-Inputs.md (strong via aliases)', () => {
132
+ describe('fixture: "RTL" → Web-Authoring-Standards.md (strong via aliases)', () => {
133
+ /**
134
+ * NOTE (119-B Task 4 alias prune, 2026-08-02): Component-Family-Form-Inputs.md
135
+ * previously carried the same RTL alias set as a second calibration fixture
136
+ * (Spec 121 Task 5). Its only RTL content was two passing bullets, so the
137
+ * aliases were replaced with form-inputs-scoped vocabulary (Lina's domain
138
+ * ruling). The RTL/internationalization recall floor is satisfied by
139
+ * Web-Authoring-Standards.md alone, which has real RTL/logical-properties
140
+ * content and keeps its aliases.
141
+ */
136
142
  let result: FindDocsHandlerResult;
137
143
 
138
144
  beforeAll(() => {
@@ -155,26 +161,11 @@ describe('find_docs calibration fixtures — real corpus, tool boundary (Task 4.
155
161
  expect(entry!.matchConfidence).toBe('strong');
156
162
  });
157
163
 
158
- it('includes Component-Family-Form-Inputs.md', () => {
159
- const entry = findEntry(result, RTL_DOC_B);
160
- expect(entry).toBeDefined();
161
- });
162
-
163
- it('classifies Component-Family-Form-Inputs.md as strong (aliases high-signal hit)', () => {
164
- const entry = findEntry(result, RTL_DOC_B);
165
- expect(entry!.matchConfidence).toBe('strong');
166
- });
167
-
168
164
  it('emits three distinct Layer fields on Web-Authoring-Standards.md (criterion b)', () => {
169
165
  const entry = findEntry(result, RTL_DOC_A);
170
166
  assertThreeDistinctLayers(entry!);
171
167
  });
172
168
 
173
- it('emits three distinct Layer fields on Component-Family-Form-Inputs.md (criterion b)', () => {
174
- const entry = findEntry(result, RTL_DOC_B);
175
- assertThreeDistinctLayers(entry!);
176
- });
177
-
178
169
  it('matchedOn for the aliases hit contains "aliases:rtl" (auditable evidence)', () => {
179
170
  const entry = findEntry(result, RTL_DOC_A);
180
171
  // aliases are high-signal; the token "rtl" must appear in matchedOn as an aliases hit
@@ -186,7 +177,7 @@ describe('find_docs calibration fixtures — real corpus, tool boundary (Task 4.
186
177
  // Fixture 2 — internationalization
187
178
  // -------------------------------------------------------------------------
188
179
 
189
- describe('fixture: "internationalization" → same two docs (strong)', () => {
180
+ describe('fixture: "internationalization" → Web-Authoring-Standards.md (strong)', () => {
190
181
  let result: FindDocsHandlerResult;
191
182
 
192
183
  beforeAll(() => {
@@ -199,12 +190,6 @@ describe('find_docs calibration fixtures — real corpus, tool boundary (Task 4.
199
190
  expect(entry!.matchConfidence).toBe('strong');
200
191
  });
201
192
 
202
- it('includes Component-Family-Form-Inputs.md at strong', () => {
203
- const entry = findEntry(result, RTL_DOC_B);
204
- expect(entry).toBeDefined();
205
- expect(entry!.matchConfidence).toBe('strong');
206
- });
207
-
208
193
  it('emits three distinct Layer fields (criterion b)', () => {
209
194
  const entry = findEntry(result, RTL_DOC_A);
210
195
  assertThreeDistinctLayers(entry!);