@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.
- package/.kiro/agents/ada-prompt.md +80 -132
- package/.kiro/agents/ada-prompt.md.attribution.json +45 -0
- package/.kiro/agents/ada.json +44 -59
- package/.kiro/agents/ada.json.attribution.json +13 -0
- package/.kiro/agents/data-prompt.md +83 -74
- package/.kiro/agents/data-prompt.md.attribution.json +53 -0
- package/.kiro/agents/data.json +31 -38
- package/.kiro/agents/data.json.attribution.json +13 -0
- package/.kiro/agents/kenya-prompt.md +83 -72
- package/.kiro/agents/kenya-prompt.md.attribution.json +53 -0
- package/.kiro/agents/kenya.json +27 -35
- package/.kiro/agents/kenya.json.attribution.json +13 -0
- package/.kiro/agents/leonardo-prompt.md +176 -234
- package/.kiro/agents/leonardo-prompt.md.attribution.json +45 -0
- package/.kiro/agents/leonardo.json +28 -36
- package/.kiro/agents/leonardo.json.attribution.json +13 -0
- package/.kiro/agents/lina-prompt.md +110 -151
- package/.kiro/agents/lina-prompt.md.attribution.json +53 -0
- package/.kiro/agents/lina.json +46 -59
- package/.kiro/agents/lina.json.attribution.json +13 -0
- package/.kiro/agents/sparky-prompt.md +89 -72
- package/.kiro/agents/sparky-prompt.md.attribution.json +53 -0
- package/.kiro/agents/sparky.json +37 -37
- package/.kiro/agents/sparky.json.attribution.json +13 -0
- package/.kiro/agents/stacy-prompt.md +74 -48
- package/.kiro/agents/stacy-prompt.md.attribution.json +45 -0
- package/.kiro/agents/stacy.json +28 -30
- package/.kiro/agents/stacy.json.attribution.json +13 -0
- package/.kiro/agents/thurgood-prompt.md +94 -138
- package/.kiro/agents/thurgood-prompt.md.attribution.json +45 -0
- package/.kiro/agents/thurgood.json +31 -35
- package/.kiro/agents/thurgood.json.attribution.json +13 -0
- package/.kiro/steering/AI-Collaboration-Principles.md +3 -3
- package/.kiro/steering/Civitas-System-Overview.md +7 -16
- package/.kiro/steering/DesignerPunk-Systems-Overview.md +6 -6
- package/.kiro/steering/Spec-Feedback-Protocol.md +2 -11
- package/.kiro/steering/Task-Completion-Protocol.md +98 -17
- package/.kiro/steering/core-goals.md +3 -3
- package/.kiro/steering/personal-note.md +1 -1
- package/.kiro/steering/start-up-tasks.md +17 -6
- package/application-mcp-server/src/index.ts +26 -0
- package/dist/ComponentTokens.android.kt +12 -12
- package/dist/ComponentTokens.ios.swift +12 -12
- package/dist/ComponentTokens.web.css +3 -3
- package/dist/DesignTokens.android.kt +1 -1
- package/dist/DesignTokens.dtcg.json +8 -5
- package/dist/DesignTokens.figma.json +2 -2
- package/dist/DesignTokens.ios.swift +1 -1
- package/dist/DesignTokens.web.css +1 -1
- package/dist/android/DesignTokens.android.kt +1 -1
- package/dist/blend/OklchBlendCalculator.js +1 -0
- package/dist/blend/ThemeAwareBlendUtilities.web.d.ts +13 -2
- package/dist/blend/ThemeAwareBlendUtilities.web.js +6 -1
- package/dist/browser/designerpunk.esm.js +36 -90
- package/dist/browser/designerpunk.esm.min.js +33 -36
- package/dist/browser/designerpunk.umd.js +36 -90
- package/dist/browser/designerpunk.umd.min.js +47 -50
- package/dist/browser/tokens.css +3 -3
- package/dist/build/tokens/defineComponentTokens.d.ts +10 -0
- package/dist/build/tokens/defineComponentTokens.js +26 -0
- package/dist/components/core/Avatar-Base/avatar.tokens.d.ts +21 -26
- package/dist/components/core/Avatar-Base/avatar.tokens.js +31 -34
- package/dist/components/core/Avatar-Base/index.d.ts +1 -1
- package/dist/components/core/Avatar-Base/index.js +2 -2
- package/dist/components/core/Avatar-Base/platforms/web/Avatar.web.js +24 -5
- package/dist/components/core/Button-CTA/examples/BasicUsage.d.ts +16 -28
- package/dist/components/core/Button-CTA/examples/BasicUsage.js +18 -43
- package/dist/components/core/Button-CTA/platforms/web/ButtonCTA.web.d.ts +3 -15
- package/dist/components/core/Button-CTA/platforms/web/ButtonCTA.web.js +9 -58
- package/dist/components/core/Button-CTA/types.d.ts +0 -24
- package/dist/components/core/Button-CTA/types.js +6 -0
- package/dist/components/core/Button-Icon/buttonIcon.tokens.d.ts +28 -14
- package/dist/components/core/Button-Icon/buttonIcon.tokens.js +35 -20
- package/dist/components/core/Input-Text-Base/types.d.ts +13 -1
- package/dist/components/core/Input-Text-Password/platforms/web/InputTextPassword.web.js +11 -2
- package/dist/generators/DTCGFormatGenerator.js +8 -0
- package/dist/generators/TokenFileGenerator.js +7 -2
- package/dist/integration/BuildErrorHandler.js +2 -2
- package/dist/ios/DesignTokens.ios.swift +1 -1
- package/dist/mcp/application-mcp.js +24 -0
- package/dist/mcp/docs-mcp.js +130 -15
- package/dist/mcp/product-mcp.js +25 -0
- package/dist/tokens/OpacityTokens.js +1 -1
- package/dist/tokens/component/progress.d.ts +65 -5
- package/dist/tokens/component/progress.js +79 -18
- package/dist/tokens/semantic/BlendTokens.d.ts +10 -3
- package/dist/tokens/semantic/BlendTokens.js +17 -5
- package/dist/tokens/semantic/OpacityTokens.d.ts +4 -4
- package/dist/tokens/semantic/OpacityTokens.js +4 -4
- package/dist/types/ComponentTypes.d.ts +1 -1
- package/dist/types/generated/TokenTypes.d.ts +1 -1
- package/dist/types/generated/TokenTypes.js +1 -1
- package/dist/validators/StemmaTokenUsageValidator.js +3 -2
- package/dist/web/DesignTokens.web.css +1 -1
- package/governance/BUILD-SYSTEM-SETUP.md +1 -2
- package/governance/Component-Development-Guide.md +23 -13
- package/governance/Component-Development-Standards.md +21 -20
- package/governance/Component-Family-Avatar.md +6 -7
- package/governance/Component-Family-Badge.md +19 -20
- package/governance/Component-Family-Button.md +30 -43
- package/governance/Component-Family-Chip.md +14 -15
- package/governance/Component-Family-Container.md +12 -13
- package/governance/Component-Family-Data-Display.md +1 -2
- package/governance/Component-Family-Divider.md +1 -2
- package/governance/Component-Family-Form-Inputs.md +65 -65
- package/governance/Component-Family-Icon.md +9 -10
- package/governance/Component-Family-Loading.md +1 -2
- package/governance/Component-Family-Modal.md +1 -2
- package/governance/Component-Family-Navigation.md +1 -2
- package/governance/Component-Family-Progress.md +0 -1
- package/governance/Component-Inheritance-Structures.md +219 -99
- package/governance/Component-MCP-Document-Template.md +6 -5
- package/governance/Component-Primitive-vs-Semantic-Philosophy.md +1 -1
- package/governance/Component-Quick-Reference.md +33 -33
- package/governance/Component-Readiness-Status.md +59 -44
- package/governance/Component-Templates.md +57 -61
- package/governance/Contract-System-Reference.md +7 -7
- package/governance/MCP-Integration-Guide.md +1 -1
- package/governance/Process-Cross-Reference-Standards.md +31 -13
- package/governance/Process-Development-Workflow.md +49 -59
- package/governance/Process-File-Organization.md +25 -28
- package/governance/Process-Hook-Operations.md +22 -11
- package/governance/Process-Orchestration-Model-Selection.md +92 -0
- package/governance/Process-Spec-Planning.md +98 -52
- package/governance/Process-Task-Type-Definitions.md +80 -4
- package/governance/Product-Handoff-Protocol.md +2 -0
- package/governance/Rosetta-System-Architecture.md +13 -11
- package/governance/Test-Behavioral-Contract-Validation.md +38 -31
- package/governance/Test-Failure-Audit-Methodology.md +1 -1
- package/governance/Token-Family-Accessibility.md +1 -2
- package/governance/Token-Family-Blend.md +18 -16
- package/governance/Token-Family-Blur.md +0 -1
- package/governance/Token-Family-Border.md +1 -2
- package/governance/Token-Family-Color.md +0 -1
- package/governance/Token-Family-Glow.md +1 -2
- package/governance/Token-Family-Layering.md +0 -1
- package/governance/Token-Family-Motion.md +1 -2
- package/governance/Token-Family-Opacity.md +0 -1
- package/governance/Token-Family-Radius.md +1 -2
- package/governance/Token-Family-Responsive.md +1 -2
- package/governance/Token-Family-Shadow.md +1 -2
- package/governance/Token-Family-Sizing.md +0 -1
- package/governance/Token-Family-Spacing.md +1 -2
- package/governance/Token-Family-Typography.md +1 -2
- package/governance/Token-Governance.md +8 -8
- package/governance/Token-Quick-Reference.md +47 -34
- package/governance/Token-Resolution-Patterns.md +1 -1
- package/governance/Token-Semantic-Structure.md +1 -1
- package/governance/Web-Authoring-Standards.md +5 -5
- package/governance/browser-distribution-guide.md +1 -4
- package/governance/classification-map.md +474 -0
- package/governance/completion-documentation-guide.md +23 -37
- package/governance/component-meta-authoring-guide.md +1 -1
- package/governance/cross-platform-vs-platform-specific-decision-framework.md +1 -1
- package/governance/platform-implementation-guidelines.md +2 -3
- package/governance/release-management-system.md +28 -63
- package/governance/rosetta-system-principles.md +8 -6
- package/governance/stemma-system-principles.md +18 -17
- package/mcp-server/src/index.ts +24 -6
- package/mcp-server/src/indexer/DocumentIndexer.ts +119 -9
- package/mcp-server/src/indexer/__tests__/bare-id-crossrefs.test.ts +250 -0
- package/mcp-server/src/indexer/cross-ref-parser.ts +29 -1
- package/mcp-server/src/indexer/index-health.ts +27 -2
- package/mcp-server/src/query/__tests__/find-docs-calibration.test.ts +11 -26
- package/mcp-server/src/relocation-integrity-gate/__tests__/relocation-integrity-gate.test.ts +72 -5
- package/mcp-server/src/relocation-integrity-gate/relocation-integrity-gate.ts +81 -24
- package/mcp-server/src/tools/list-cross-references.ts +2 -2
- package/package.json +24 -26
- package/src/__tests__/browser-distribution/css-bundling.test.ts +6 -4
- package/src/__tests__/console-allowlist.json +14 -0
- package/src/__tests__/console-fail-setup.ts +169 -0
- package/src/__tests__/integration/Spec107-DesignLanguageContext.test.ts +16 -0
- package/src/__tests__/stemma-system/behavioral-contract-validation.test.ts +70 -17
- package/src/__tests__/stemma-system/contract-catalog-name-validation.test.ts +28 -0
- package/src/__tests__/stemma-system/form-inputs-contracts.test.ts +223 -16
- package/src/__tests__/stemma-system/input-text-native-base-call-alignment.test.ts +298 -0
- package/src/blend/OklchBlendCalculator.ts +3 -0
- package/src/blend/ThemeAwareBlendUtilities.android.kt +3 -0
- package/src/blend/ThemeAwareBlendUtilities.ios.swift +3 -0
- package/src/blend/ThemeAwareBlendUtilities.web.ts +9 -1
- package/src/blend/__tests__/InteractionStateAudit.test.ts +12 -9
- package/src/build/errors/__tests__/ErrorHandler.integration.test.ts +8 -0
- package/src/build/errors/__tests__/ErrorHandler.test.ts +5 -0
- package/src/build/tokens/__tests__/defineComponentTokens.test.ts +113 -0
- package/src/build/tokens/defineComponentTokens.ts +43 -1
- package/src/build/workflow/__tests__/CICDIntegration.test.ts +12 -1
- package/src/cli/__tests__/init.test.ts +45 -11
- package/src/components/core/Avatar-Base/Avatar-Base.schema.yaml +1 -1
- package/src/components/core/Avatar-Base/__tests__/Avatar.accessibility.test.ts +121 -7
- package/src/components/core/Avatar-Base/__tests__/Avatar.image.test.ts +3 -0
- package/src/components/core/Avatar-Base/__tests__/Avatar.test.ts +15 -6
- package/src/components/core/Avatar-Base/avatar.tokens.ts +31 -34
- package/src/components/core/Avatar-Base/contracts.yaml +11 -1
- package/src/components/core/Avatar-Base/index.ts +1 -1
- package/src/components/core/Avatar-Base/platforms/web/Avatar.web.ts +24 -5
- package/src/components/core/Badge-Count-Base/contracts.yaml +1 -1
- package/src/components/core/Badge-Label-Base/contracts.yaml +1 -1
- package/src/components/core/Button-CTA/Button-CTA.schema.yaml +2 -12
- package/src/components/core/Button-CTA/README.md +3 -6
- package/src/components/core/Button-CTA/__tests__/ButtonCTA.test.ts +35 -89
- package/src/components/core/Button-CTA/__tests__/setup.test.ts +0 -2
- package/src/components/core/Button-CTA/__tests__/test-utils.ts +0 -2
- package/src/components/core/Button-CTA/contracts.yaml +6 -29
- package/src/components/core/Button-CTA/examples/BasicUsage.html +2 -14
- package/src/components/core/Button-CTA/examples/BasicUsage.tsx +17 -44
- package/src/components/core/Button-CTA/platforms/android/ButtonCTA.android.kt +12 -20
- package/src/components/core/Button-CTA/platforms/ios/ButtonCTA.ios.swift +12 -51
- package/src/components/core/Button-CTA/platforms/web/ButtonCTA.web.css +2 -26
- package/src/components/core/Button-CTA/platforms/web/ButtonCTA.web.ts +18 -71
- package/src/components/core/Button-CTA/types.ts +10 -28
- package/src/components/core/Button-Icon/buttonIcon.tokens.ts +43 -27
- package/src/components/core/Chip-Base/__tests__/ChipBase.test.ts +13 -0
- package/src/components/core/Chip-Filter/__tests__/ChipFilter.test.ts +13 -0
- package/src/components/core/Chip-Input/__tests__/ChipInput.test.ts +13 -0
- package/src/components/core/Input-Text-Base/Input-Text-Base.schema.yaml +30 -2
- package/src/components/core/Input-Text-Base/README.md +25 -2
- package/src/components/core/Input-Text-Base/__tests__/focusIndicators.test.ts +16 -15
- package/src/components/core/Input-Text-Base/contracts.yaml +90 -0
- package/src/components/core/Input-Text-Base/platforms/android/InputTextBase.android.kt +26 -12
- package/src/components/core/Input-Text-Base/platforms/ios/InputTextBase.ios.swift +195 -59
- package/src/components/core/Input-Text-Base/types.ts +13 -1
- package/src/components/core/Input-Text-Email/Input-Text-Email.schema.yaml +5 -1
- package/src/components/core/Input-Text-Email/README.md +8 -7
- package/src/components/core/Input-Text-Email/platforms/android/InputTextEmail.android.kt +1 -4
- package/src/components/core/Input-Text-Email/platforms/ios/InputTextEmail.ios.swift +2 -16
- package/src/components/core/Input-Text-Password/Input-Text-Password.schema.yaml +10 -3
- package/src/components/core/Input-Text-Password/README.md +9 -8
- package/src/components/core/Input-Text-Password/contracts.yaml +5 -0
- package/src/components/core/Input-Text-Password/platforms/android/InputTextPassword.android.kt +17 -7
- package/src/components/core/Input-Text-Password/platforms/ios/InputTextPassword.ios.swift +22 -20
- package/src/components/core/Input-Text-Password/platforms/web/InputTextPassword.web.ts +11 -2
- package/src/components/core/Input-Text-PhoneNumber/Input-Text-PhoneNumber.schema.yaml +5 -1
- package/src/components/core/Input-Text-PhoneNumber/README.md +9 -8
- package/src/components/core/Input-Text-PhoneNumber/platforms/android/InputTextPhoneNumber.android.kt +2 -5
- package/src/components/core/Input-Text-PhoneNumber/platforms/ios/InputTextPhoneNumber.ios.swift +3 -17
- package/src/components/core/Nav-Header-App/contracts.yaml +1 -1
- package/src/components/core/Nav-SegmentedChoice-Base/contracts.yaml +1 -1
- package/src/components/core/Progress-Indicator-Connector-Base/contracts.yaml +1 -1
- package/src/components/core/Progress-Indicator-Label-Base/contracts.yaml +1 -1
- package/src/components/core/Progress-Indicator-Node-Base/contracts.yaml +1 -1
- package/src/components/core/Progress-Stepper-Base/__tests__/StepperBase.test.ts +5 -2
- package/src/components/core/Progress-Stepper-Detailed/__tests__/StepperDetailed.test.ts +5 -2
- package/src/generators/DTCGFormatGenerator.ts +6 -0
- package/src/generators/TokenFileGenerator.ts +7 -2
- package/src/generators/__tests__/DTCGConfigOptions.test.ts +14 -5
- package/src/integration/BuildErrorHandler.ts +2 -2
- package/src/tokens/OpacityTokens.ts +1 -1
- package/src/tokens/__tests__/OpacityTokens.test.ts +3 -1
- package/src/tokens/__tests__/ProgressTokenCompliance.test.ts +5 -3
- package/src/tokens/__tests__/ProgressTokenFormulas.test.ts +11 -11
- package/src/tokens/__tests__/ProgressTokenTranslation.test.ts +22 -20
- package/src/tokens/component/progress.ts +83 -21
- package/src/tokens/semantic/BlendTokens.ts +26 -5
- package/src/tokens/semantic/OpacityTokens.ts +4 -4
- package/src/types/ComponentTypes.ts +1 -1
- package/src/types/generated/TokenTypes.ts +1 -1
- package/src/validators/StemmaTokenUsageValidator.ts +3 -2
- package/token-index/components.yaml +8 -8
- package/token-index/semantics.yaml +1 -2
- package/src/tools/release/__tests__/ChangeClassifier.test.ts +0 -133
- package/src/tools/release/__tests__/ChangeExtractor.test.ts +0 -222
- package/src/tools/release/__tests__/GitHubPublisher.test.ts +0 -240
- package/src/tools/release/__tests__/NotesRenderer.test.ts +0 -142
- package/src/tools/release/__tests__/NpmPublisher.test.ts +0 -289
- package/src/tools/release/__tests__/PipelineIntegration.test.ts +0 -188
- package/src/tools/release/__tests__/ReleasePipeline.test.ts +0 -192
- package/src/tools/release/__tests__/SemanticVersionValidator.test.ts +0 -49
- package/src/tools/release/__tests__/SummaryScanner.test.ts +0 -141
- package/src/tools/release/__tests__/TagResolver.test.ts +0 -91
- package/src/tools/release/__tests__/VersionCalculator.test.ts +0 -270
- package/src/tools/release/__tests__/helpers/NpmMockHelper.ts +0 -80
- package/src/tools/release/cli/ReleasePipeline.ts +0 -165
- package/src/tools/release/cli/release-tool.ts +0 -107
- package/src/tools/release/pipeline/ChangeClassifier.ts +0 -61
- package/src/tools/release/pipeline/ChangeExtractor.ts +0 -87
- package/src/tools/release/pipeline/NotesRenderer.ts +0 -66
- package/src/tools/release/pipeline/SummaryScanner.ts +0 -70
- package/src/tools/release/pipeline/TagResolver.ts +0 -40
- package/src/tools/release/pipeline/VersionCalculator.ts +0 -375
- package/src/tools/release/publishers/GitHubPublisher.ts +0 -228
- package/src/tools/release/publishers/NpmPublisher.ts +0 -196
- package/src/tools/release/release-config.json +0 -5
- package/src/tools/release/types/index.ts +0 -282
- 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
|
|
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
|
-
//
|
|
216
|
-
|
|
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
|
-
*
|
|
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
|
|
358
|
-
return
|
|
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
|
|
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" →
|
|
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!);
|