@dzhechkov/harness-core 0.8.6 → 0.8.11
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/.dz-manifest.json +361 -97
- package/README.md +48 -1
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +12 -1
- package/dist/amendment-trace.js.map +1 -1
- package/dist/codex-invoke.d.ts +73 -0
- package/dist/codex-invoke.d.ts.map +1 -0
- package/dist/codex-invoke.js +80 -0
- package/dist/codex-invoke.js.map +1 -0
- package/dist/compounding.d.ts +54 -0
- package/dist/compounding.d.ts.map +1 -1
- package/dist/compounding.js +221 -1
- package/dist/compounding.js.map +1 -1
- package/dist/discrimination-gate.d.ts +63 -3
- package/dist/discrimination-gate.d.ts.map +1 -1
- package/dist/discrimination-gate.js +113 -16
- package/dist/discrimination-gate.js.map +1 -1
- package/dist/eta.d.ts +92 -0
- package/dist/eta.d.ts.map +1 -0
- package/dist/eta.js +488 -0
- package/dist/eta.js.map +1 -0
- package/dist/event-chain.d.ts +30 -0
- package/dist/event-chain.d.ts.map +1 -1
- package/dist/event-chain.js +24 -0
- package/dist/event-chain.js.map +1 -1
- package/dist/feature-adr-checkpoints.js +1 -1
- package/dist/feature-adr-decision-recall.d.ts +167 -0
- package/dist/feature-adr-decision-recall.d.ts.map +1 -0
- package/dist/feature-adr-decision-recall.js +519 -0
- package/dist/feature-adr-decision-recall.js.map +1 -0
- package/dist/feature-adr-landing.d.ts +37 -0
- package/dist/feature-adr-landing.d.ts.map +1 -0
- package/dist/feature-adr-landing.js +59 -0
- package/dist/feature-adr-landing.js.map +1 -0
- package/dist/feature-adr-routing.d.ts +2 -2
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +7 -11
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard-promotion.d.ts +41 -0
- package/dist/guard-promotion.d.ts.map +1 -1
- package/dist/guard-promotion.js +218 -4
- package/dist/guard-promotion.js.map +1 -1
- package/dist/guard-volume.d.ts +108 -0
- package/dist/guard-volume.d.ts.map +1 -0
- package/dist/guard-volume.js +536 -0
- package/dist/guard-volume.js.map +1 -0
- package/dist/guard.d.ts +17 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +92 -4
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +24 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -5
- package/dist/index.js.map +1 -1
- package/dist/integration-apply.d.ts +25 -0
- package/dist/integration-apply.d.ts.map +1 -0
- package/dist/integration-apply.js +299 -0
- package/dist/integration-apply.js.map +1 -0
- package/dist/integration-evidence.d.ts +46 -0
- package/dist/integration-evidence.d.ts.map +1 -0
- package/dist/integration-evidence.js +44 -0
- package/dist/integration-evidence.js.map +1 -0
- package/dist/integration-probe-worker.d.ts +22 -0
- package/dist/integration-probe-worker.d.ts.map +1 -0
- package/dist/integration-probe-worker.js +334 -0
- package/dist/integration-probe-worker.js.map +1 -0
- package/dist/integrations-verify.d.ts +60 -0
- package/dist/integrations-verify.d.ts.map +1 -0
- package/dist/integrations-verify.js +194 -0
- package/dist/integrations-verify.js.map +1 -0
- package/dist/lesson-generalization.d.ts +29 -0
- package/dist/lesson-generalization.d.ts.map +1 -0
- package/dist/lesson-generalization.js +84 -0
- package/dist/lesson-generalization.js.map +1 -0
- package/dist/mutation-gate.d.ts +39 -36
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +111 -5
- package/dist/mutation-gate.js.map +1 -1
- package/dist/operations.d.ts +27 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +186 -7
- package/dist/operations.js.map +1 -1
- package/dist/patterns.d.ts +27 -1
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +211 -45
- package/dist/patterns.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +27 -5
- package/dist/plugin.js.map +1 -1
- package/dist/recommend.d.ts +4 -5
- package/dist/recommend.d.ts.map +1 -1
- package/dist/recommend.js +110 -45
- package/dist/recommend.js.map +1 -1
- package/dist/registry.d.ts +32 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +165 -9
- package/dist/registry.js.map +1 -1
- package/dist/run-records.d.ts +3 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +18 -0
- package/dist/run-records.js.map +1 -1
- package/dist/score.d.ts +95 -0
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +274 -2
- package/dist/score.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +20 -17
- package/dist/setup.js.map +1 -1
- package/dist/skill-selection.d.ts +72 -0
- package/dist/skill-selection.d.ts.map +1 -0
- package/dist/skill-selection.js +76 -0
- package/dist/skill-selection.js.map +1 -0
- package/dist/stem.d.ts +12 -0
- package/dist/stem.d.ts.map +1 -0
- package/dist/stem.js +89 -0
- package/dist/stem.js.map +1 -0
- package/dist/target-integrations.d.ts +65 -0
- package/dist/target-integrations.d.ts.map +1 -0
- package/dist/target-integrations.js +152 -0
- package/dist/target-integrations.js.map +1 -0
- package/dist/telemetry-vocabulary.d.ts +7 -0
- package/dist/telemetry-vocabulary.d.ts.map +1 -1
- package/dist/telemetry-vocabulary.js +29 -0
- package/dist/telemetry-vocabulary.js.map +1 -1
- package/dist/vector-tier.d.ts +6 -1
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +32 -7
- package/dist/vector-tier.js.map +1 -1
- package/package.json +7 -6
- package/sbom.json +772 -112
- package/src/amendment-trace.ts +12 -1
- package/src/codex-invoke.ts +138 -0
- package/src/compounding.ts +300 -1
- package/src/discrimination-gate.ts +183 -19
- package/src/eta.ts +590 -0
- package/src/event-chain.ts +41 -0
- package/src/feature-adr-checkpoints.ts +1 -1
- package/src/feature-adr-decision-recall.ts +652 -0
- package/src/feature-adr-landing.ts +109 -0
- package/src/feature-adr-routing.ts +7 -11
- package/src/guard-promotion.ts +245 -4
- package/src/guard-volume.ts +752 -0
- package/src/guard.ts +110 -4
- package/src/index.ts +73 -6
- package/src/integration-apply.ts +332 -0
- package/src/integration-evidence.ts +89 -0
- package/src/integration-probe-worker.ts +310 -0
- package/src/integration-receipts/claude-code/mcp/2.1.235.json +35 -0
- package/src/integrations-verify.ts +258 -0
- package/src/lesson-generalization.ts +115 -0
- package/src/mutation-gate.ts +165 -5
- package/src/operations.ts +207 -7
- package/src/patterns.ts +252 -43
- package/src/plugin.ts +27 -5
- package/src/recommend.ts +116 -46
- package/src/registry.ts +144 -11
- package/src/run-records.ts +23 -0
- package/src/score.ts +361 -3
- package/src/setup.ts +20 -17
- package/src/skill-selection.ts +111 -0
- package/src/stem.ts +87 -0
- package/src/target-integrations.ts +225 -0
- package/src/telemetry-vocabulary.ts +36 -0
- package/src/vector-tier.ts +44 -14
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
export const VOLUME_SHADOW_SCHEMA_VERSION = 'volume-shadow/v1' as const;
|
|
2
|
+
|
|
3
|
+
export const VOLUME_SHADOW_RULE_IDS = [
|
|
4
|
+
'template-context-token-weight',
|
|
5
|
+
'template-context-largest-file-share',
|
|
6
|
+
'feature-artifact-diff-ratio',
|
|
7
|
+
'feature-tier-artifact-set',
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
export type VolumeShadowRuleId = typeof VOLUME_SHADOW_RULE_IDS[number];
|
|
11
|
+
export type FeatureTier = 'S' | 'M' | 'L' | 'XL';
|
|
12
|
+
export type VolumeObservationStatus = 'measured' | 'within-reference' | 'outside-reference' | 'unknown';
|
|
13
|
+
|
|
14
|
+
export const VOLUME_SCOPE = {
|
|
15
|
+
included: [
|
|
16
|
+
'templates/.claude/rules/*.md',
|
|
17
|
+
'templates/.claude/commands/*.md',
|
|
18
|
+
'templates/.claude/skills/**/SKILL.md',
|
|
19
|
+
'features/<slug>/00-09*.md',
|
|
20
|
+
'features/<slug>/03_adr/**',
|
|
21
|
+
'features/<slug>/07_code_changes/**',
|
|
22
|
+
],
|
|
23
|
+
excluded: [
|
|
24
|
+
'comment-code-density',
|
|
25
|
+
'comment-count',
|
|
26
|
+
'code-line-count',
|
|
27
|
+
'prose-classification',
|
|
28
|
+
'src/**',
|
|
29
|
+
'test/**',
|
|
30
|
+
],
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
export interface VolumeReference {
|
|
34
|
+
readonly kind: 'measured-starting-point' | 'pipeline-contract';
|
|
35
|
+
readonly source: string;
|
|
36
|
+
readonly measuredAt?: string;
|
|
37
|
+
readonly sampleSize?: number;
|
|
38
|
+
readonly low?: number;
|
|
39
|
+
readonly high?: number;
|
|
40
|
+
readonly numerator?: number;
|
|
41
|
+
readonly denominator?: number;
|
|
42
|
+
readonly expected?: readonly string[];
|
|
43
|
+
readonly caveat?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type ObservationOperand = number | string | null | readonly string[];
|
|
47
|
+
|
|
48
|
+
export interface GuardObservation {
|
|
49
|
+
readonly schemaVersion: typeof VOLUME_SHADOW_SCHEMA_VERSION;
|
|
50
|
+
readonly rule: VolumeShadowRuleId;
|
|
51
|
+
readonly metric: string;
|
|
52
|
+
readonly scope: string;
|
|
53
|
+
readonly status: VolumeObservationStatus;
|
|
54
|
+
readonly value: number | readonly string[] | null;
|
|
55
|
+
readonly unit: 'estimated_tokens' | 'fraction_of_corpus' | 'byte_ratio' | 'artifact_set';
|
|
56
|
+
readonly signal: boolean;
|
|
57
|
+
readonly operands: Readonly<Record<string, ObservationOperand>>;
|
|
58
|
+
readonly reference?: VolumeReference;
|
|
59
|
+
readonly method: string;
|
|
60
|
+
readonly detail: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface VolumeCollectionState {
|
|
64
|
+
readonly complete: boolean;
|
|
65
|
+
readonly reason?: string;
|
|
66
|
+
readonly detail?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface TemplateVolumeFileFact {
|
|
70
|
+
readonly path: string;
|
|
71
|
+
readonly kind: string;
|
|
72
|
+
readonly bytes: number;
|
|
73
|
+
readonly cyrillicUtf8Bytes: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface TemplateVolumeTargetFact {
|
|
77
|
+
readonly target: string;
|
|
78
|
+
readonly files: readonly TemplateVolumeFileFact[];
|
|
79
|
+
readonly collection?: VolumeCollectionState;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface FeatureArtifactFact {
|
|
83
|
+
readonly path: string;
|
|
84
|
+
readonly bytes: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface FeatureDiffFact {
|
|
88
|
+
readonly attributable: boolean;
|
|
89
|
+
readonly bytes?: number;
|
|
90
|
+
readonly base?: string;
|
|
91
|
+
readonly head?: string;
|
|
92
|
+
readonly method?: string;
|
|
93
|
+
readonly excludedFeaturePath?: string;
|
|
94
|
+
readonly reason?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface FeatureVolumeFact {
|
|
98
|
+
readonly slug: string;
|
|
99
|
+
readonly tier?: FeatureTier;
|
|
100
|
+
readonly activeSteps?: readonly (number | string)[];
|
|
101
|
+
readonly namedConsumers?: readonly string[];
|
|
102
|
+
readonly lifecycle?: { readonly phase: 'in-progress' | 'complete'; readonly completedThroughStep?: number };
|
|
103
|
+
readonly artifacts: readonly FeatureArtifactFact[];
|
|
104
|
+
readonly diff?: FeatureDiffFact;
|
|
105
|
+
readonly collection?: VolumeCollectionState;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface VolumeShadowInput {
|
|
109
|
+
readonly templates?: readonly TemplateVolumeTargetFact[];
|
|
110
|
+
readonly features?: readonly FeatureVolumeFact[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface VolumeShadowSignal {
|
|
114
|
+
readonly rule: VolumeShadowRuleId;
|
|
115
|
+
readonly detail: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface VolumeShadowResult {
|
|
119
|
+
readonly observations: readonly GuardObservation[];
|
|
120
|
+
readonly signals: readonly VolumeShadowSignal[];
|
|
121
|
+
readonly notes: readonly string[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type TemplateTokenEstimate =
|
|
125
|
+
| {
|
|
126
|
+
readonly status: 'measured';
|
|
127
|
+
readonly bytes: number;
|
|
128
|
+
readonly cyrillicUtf8Bytes: number;
|
|
129
|
+
readonly divisor: 2.5 | 4;
|
|
130
|
+
readonly estimatedTokens: number;
|
|
131
|
+
readonly method: 'utf8-bytes-divisor/v1';
|
|
132
|
+
}
|
|
133
|
+
| { readonly status: 'unknown'; readonly reason: string };
|
|
134
|
+
|
|
135
|
+
const MEASUREMENT_SOURCE = 'docs/research/volume-density-2026-08-30/01-measurement.md';
|
|
136
|
+
const MEASURED_TARGET = '@dzhechkov/p-replicator';
|
|
137
|
+
const TOKEN_WEIGHT_START = 80_755;
|
|
138
|
+
const LARGEST_NUMERATOR_START = 11_730;
|
|
139
|
+
const LARGEST_DENOMINATOR_START = 80_755;
|
|
140
|
+
const ARTIFACT_RATIO_LOW = 1.03;
|
|
141
|
+
const ARTIFACT_RATIO_HIGH = 2.06;
|
|
142
|
+
const EMPTY_RESULT: VolumeShadowResult = { observations: [], signals: [], notes: [] };
|
|
143
|
+
|
|
144
|
+
const TEMPLATE_REFERENCE: VolumeReference = {
|
|
145
|
+
kind: 'measured-starting-point',
|
|
146
|
+
source: MEASUREMENT_SOURCE,
|
|
147
|
+
measuredAt: '2026-08-30',
|
|
148
|
+
sampleSize: 28,
|
|
149
|
+
high: TOKEN_WEIGHT_START,
|
|
150
|
+
caveat: 'rules and commands are standing context; full skill bodies are conditional on invocation',
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const LARGEST_REFERENCE: VolumeReference = {
|
|
154
|
+
kind: 'measured-starting-point',
|
|
155
|
+
source: MEASUREMENT_SOURCE,
|
|
156
|
+
measuredAt: '2026-08-30',
|
|
157
|
+
sampleSize: 28,
|
|
158
|
+
numerator: LARGEST_NUMERATOR_START,
|
|
159
|
+
denominator: LARGEST_DENOMINATOR_START,
|
|
160
|
+
high: LARGEST_NUMERATOR_START / LARGEST_DENOMINATOR_START,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const RATIO_REFERENCE: VolumeReference = {
|
|
164
|
+
kind: 'measured-starting-point',
|
|
165
|
+
source: MEASUREMENT_SOURCE,
|
|
166
|
+
measuredAt: '2026-08-30',
|
|
167
|
+
sampleSize: 5,
|
|
168
|
+
low: ARTIFACT_RATIO_LOW,
|
|
169
|
+
high: ARTIFACT_RATIO_HIGH,
|
|
170
|
+
caveat: 'denominator is attributable unified-diff bytes, a proxy rather than net source bytes',
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const RULE_ORDER = new Map<string, number>(VOLUME_SHADOW_RULE_IDS.map((id, index) => [id, index]));
|
|
174
|
+
|
|
175
|
+
function boundedDetail(value: unknown): string {
|
|
176
|
+
const text = typeof value === 'string' && value.trim() !== '' ? value.trim() : 'evidence unavailable';
|
|
177
|
+
return text.replace(/\s+/g, ' ').slice(0, 240);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function safeRelativePath(value: unknown): string | undefined {
|
|
181
|
+
if (typeof value !== 'string' || value === '' || value.includes('\0')) return undefined;
|
|
182
|
+
const path = value.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
183
|
+
if (path.startsWith('/') || /^[A-Za-z]:\//.test(path)) return undefined;
|
|
184
|
+
const parts = path.split('/');
|
|
185
|
+
if (parts.some((part) => part === '' || part === '.' || part === '..')) return undefined;
|
|
186
|
+
return parts.join('/');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function validByteCount(value: unknown, allowZero = false): value is number {
|
|
190
|
+
return typeof value === 'number'
|
|
191
|
+
&& Number.isSafeInteger(value)
|
|
192
|
+
&& (allowZero ? value >= 0 : value > 0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function estimateTemplateTokens(bytes: unknown, cyrillicUtf8Bytes: unknown): TemplateTokenEstimate {
|
|
196
|
+
if (!validByteCount(bytes) || !validByteCount(cyrillicUtf8Bytes, true) || cyrillicUtf8Bytes > bytes) {
|
|
197
|
+
return { status: 'unknown', reason: 'invalid-byte-count' };
|
|
198
|
+
}
|
|
199
|
+
const divisor = cyrillicUtf8Bytes / bytes > 0.5 ? 2.5 : 4;
|
|
200
|
+
return {
|
|
201
|
+
status: 'measured',
|
|
202
|
+
bytes,
|
|
203
|
+
cyrillicUtf8Bytes,
|
|
204
|
+
divisor,
|
|
205
|
+
estimatedTokens: bytes / divisor,
|
|
206
|
+
method: 'utf8-bytes-divisor/v1',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function unknownObservation(
|
|
211
|
+
rule: VolumeShadowRuleId,
|
|
212
|
+
metric: string,
|
|
213
|
+
scope: string,
|
|
214
|
+
unit: GuardObservation['unit'],
|
|
215
|
+
reason: string,
|
|
216
|
+
detail?: string,
|
|
217
|
+
): GuardObservation {
|
|
218
|
+
const why = detail === undefined ? boundedDetail(reason) : `${boundedDetail(reason)}: ${boundedDetail(detail)}`;
|
|
219
|
+
return {
|
|
220
|
+
schemaVersion: VOLUME_SHADOW_SCHEMA_VERSION,
|
|
221
|
+
rule,
|
|
222
|
+
metric,
|
|
223
|
+
scope,
|
|
224
|
+
status: 'unknown',
|
|
225
|
+
value: null,
|
|
226
|
+
unit,
|
|
227
|
+
signal: false,
|
|
228
|
+
operands: { reason },
|
|
229
|
+
method: rule.startsWith('template-') ? 'utf8-bytes-divisor/v1' : 'feature-volume/v1',
|
|
230
|
+
detail: `unknown: ${why}`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function unknownTemplateTarget(scope: string, reason: string, detail?: string): GuardObservation[] {
|
|
235
|
+
return [
|
|
236
|
+
unknownObservation(
|
|
237
|
+
'template-context-token-weight',
|
|
238
|
+
'replicated-context-estimated-tokens',
|
|
239
|
+
scope,
|
|
240
|
+
'estimated_tokens',
|
|
241
|
+
reason,
|
|
242
|
+
detail,
|
|
243
|
+
),
|
|
244
|
+
unknownObservation(
|
|
245
|
+
'template-context-largest-file-share',
|
|
246
|
+
'largest-context-file-share',
|
|
247
|
+
scope,
|
|
248
|
+
'fraction_of_corpus',
|
|
249
|
+
reason,
|
|
250
|
+
detail,
|
|
251
|
+
),
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function unknownFeature(scope: string, reason: string, detail?: string): GuardObservation[] {
|
|
256
|
+
return [
|
|
257
|
+
unknownObservation(
|
|
258
|
+
'feature-artifact-diff-ratio',
|
|
259
|
+
'feature-artifact-diff-ratio',
|
|
260
|
+
scope,
|
|
261
|
+
'byte_ratio',
|
|
262
|
+
reason,
|
|
263
|
+
detail,
|
|
264
|
+
),
|
|
265
|
+
unknownObservation(
|
|
266
|
+
'feature-tier-artifact-set',
|
|
267
|
+
'feature-tier-artifacts',
|
|
268
|
+
scope,
|
|
269
|
+
'artifact_set',
|
|
270
|
+
reason,
|
|
271
|
+
detail,
|
|
272
|
+
),
|
|
273
|
+
];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function unknownVolumeShadow(reason: string, detail?: string): VolumeShadowResult {
|
|
277
|
+
const observations = [
|
|
278
|
+
...unknownTemplateTarget('volume-input', reason, detail),
|
|
279
|
+
...unknownFeature('volume-input', reason, detail),
|
|
280
|
+
];
|
|
281
|
+
return {
|
|
282
|
+
observations,
|
|
283
|
+
signals: [],
|
|
284
|
+
notes: observations.map((observation) => `${observation.rule} ${observation.scope}: ${observation.detail}`),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
type TemplateKind = 'rules' | 'commands' | 'skills';
|
|
289
|
+
|
|
290
|
+
function classifyTemplateFact(raw: unknown):
|
|
291
|
+
| { readonly status: 'excluded' }
|
|
292
|
+
| { readonly status: 'unknown'; readonly reason: string }
|
|
293
|
+
| { readonly status: 'measured'; readonly path: string; readonly kind: TemplateKind; readonly estimate: Extract<TemplateTokenEstimate, { status: 'measured' }> } {
|
|
294
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { status: 'unknown', reason: 'malformed-file-fact' };
|
|
295
|
+
const fact = raw as Partial<TemplateVolumeFileFact>;
|
|
296
|
+
if (typeof fact.kind === 'string' && (VOLUME_SCOPE.excluded as readonly string[]).includes(fact.kind)) {
|
|
297
|
+
return { status: 'excluded' };
|
|
298
|
+
}
|
|
299
|
+
const path = safeRelativePath(fact.path);
|
|
300
|
+
if (path === undefined) return { status: 'unknown', reason: 'unsafe-file-path' };
|
|
301
|
+
const kind = fact.kind === 'comment-code-density' ? 'rules' : fact.kind;
|
|
302
|
+
if (kind !== 'rules' && kind !== 'commands' && kind !== 'skills') {
|
|
303
|
+
return { status: 'unknown', reason: 'file-outside-closed-scope' };
|
|
304
|
+
}
|
|
305
|
+
const pathFits = kind === 'rules'
|
|
306
|
+
? fact.kind === 'comment-code-density' || /^templates\/\.claude\/rules\/[^/]+\.md$/.test(path)
|
|
307
|
+
: kind === 'commands'
|
|
308
|
+
? /^templates\/\.claude\/commands\/[^/]+\.md$/.test(path)
|
|
309
|
+
: /^templates\/\.claude\/skills\/.+\/SKILL\.md$/.test(path);
|
|
310
|
+
if (!pathFits) return { status: 'unknown', reason: 'file-outside-closed-scope' };
|
|
311
|
+
const estimate = estimateTemplateTokens(fact.bytes, fact.cyrillicUtf8Bytes);
|
|
312
|
+
if (estimate.status === 'unknown') return estimate;
|
|
313
|
+
return { status: 'measured', path, kind, estimate };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function evaluateTemplateTarget(raw: unknown): GuardObservation[] {
|
|
317
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
318
|
+
return unknownTemplateTarget('unknown-target', 'malformed-target-fact');
|
|
319
|
+
}
|
|
320
|
+
const target = raw as Partial<TemplateVolumeTargetFact>;
|
|
321
|
+
const scope = typeof target.target === 'string' && target.target.trim() !== ''
|
|
322
|
+
? boundedDetail(target.target)
|
|
323
|
+
: 'unknown-target';
|
|
324
|
+
if (target.collection?.complete === false) {
|
|
325
|
+
return unknownTemplateTarget(scope, boundedDetail(target.collection.reason ?? 'incomplete-template-scan'), target.collection.detail);
|
|
326
|
+
}
|
|
327
|
+
if (!Array.isArray(target.files)) return unknownTemplateTarget(scope, 'malformed-template-files');
|
|
328
|
+
|
|
329
|
+
const rows: Extract<ReturnType<typeof classifyTemplateFact>, { status: 'measured' }>[] = [];
|
|
330
|
+
const paths = new Set<string>();
|
|
331
|
+
for (const candidate of target.files) {
|
|
332
|
+
const row = classifyTemplateFact(candidate);
|
|
333
|
+
if (row.status === 'excluded') continue;
|
|
334
|
+
if (row.status === 'unknown') return unknownTemplateTarget(scope, row.reason);
|
|
335
|
+
if (paths.has(row.path)) return unknownTemplateTarget(scope, 'duplicate-template-path', row.path);
|
|
336
|
+
paths.add(row.path);
|
|
337
|
+
rows.push(row);
|
|
338
|
+
}
|
|
339
|
+
rows.sort((a, b) => a.path.localeCompare(b.path));
|
|
340
|
+
if (rows.length === 0) return unknownTemplateTarget(scope, 'empty-template-corpus');
|
|
341
|
+
|
|
342
|
+
let bytes = 0;
|
|
343
|
+
let total = 0;
|
|
344
|
+
let rules = 0;
|
|
345
|
+
let commands = 0;
|
|
346
|
+
let skills = 0;
|
|
347
|
+
let largest = rows[0]!;
|
|
348
|
+
for (const row of rows) {
|
|
349
|
+
bytes += row.estimate.bytes;
|
|
350
|
+
total += row.estimate.estimatedTokens;
|
|
351
|
+
if (row.kind === 'rules') rules += row.estimate.estimatedTokens;
|
|
352
|
+
if (row.kind === 'commands') commands += row.estimate.estimatedTokens;
|
|
353
|
+
if (row.kind === 'skills') skills += row.estimate.estimatedTokens;
|
|
354
|
+
if (row.estimate.estimatedTokens > largest.estimate.estimatedTokens) largest = row;
|
|
355
|
+
}
|
|
356
|
+
const hasReference = scope === MEASURED_TARGET;
|
|
357
|
+
const totalSignal = hasReference && total > TOKEN_WEIGHT_START;
|
|
358
|
+
const largestFraction = largest.estimate.estimatedTokens / total;
|
|
359
|
+
const largestSignal = hasReference && largestFraction > LARGEST_NUMERATOR_START / LARGEST_DENOMINATOR_START;
|
|
360
|
+
const totalStatus: VolumeObservationStatus = hasReference
|
|
361
|
+
? totalSignal ? 'outside-reference' : 'within-reference'
|
|
362
|
+
: 'measured';
|
|
363
|
+
const largestStatus: VolumeObservationStatus = hasReference
|
|
364
|
+
? largestSignal ? 'outside-reference' : 'within-reference'
|
|
365
|
+
: 'measured';
|
|
366
|
+
|
|
367
|
+
return [
|
|
368
|
+
{
|
|
369
|
+
schemaVersion: VOLUME_SHADOW_SCHEMA_VERSION,
|
|
370
|
+
rule: 'template-context-token-weight',
|
|
371
|
+
metric: 'replicated-context-estimated-tokens',
|
|
372
|
+
scope,
|
|
373
|
+
status: totalStatus,
|
|
374
|
+
value: total,
|
|
375
|
+
unit: 'estimated_tokens',
|
|
376
|
+
signal: totalSignal,
|
|
377
|
+
operands: {
|
|
378
|
+
bytes,
|
|
379
|
+
fileCount: rows.length,
|
|
380
|
+
rulesEstimatedTokens: rules,
|
|
381
|
+
commandsEstimatedTokens: commands,
|
|
382
|
+
conditionalSkillsEstimatedTokens: skills,
|
|
383
|
+
},
|
|
384
|
+
...(hasReference ? { reference: TEMPLATE_REFERENCE } : {}),
|
|
385
|
+
method: 'utf8-bytes-divisor/v1',
|
|
386
|
+
detail: `${total} estimated_tokens across ${rows.length} files; rules/commands are standing context and skill bodies are conditional on invocation`,
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
schemaVersion: VOLUME_SHADOW_SCHEMA_VERSION,
|
|
390
|
+
rule: 'template-context-largest-file-share',
|
|
391
|
+
metric: 'largest-context-file-share',
|
|
392
|
+
scope,
|
|
393
|
+
status: largestStatus,
|
|
394
|
+
value: largestFraction,
|
|
395
|
+
unit: 'fraction_of_corpus',
|
|
396
|
+
signal: largestSignal,
|
|
397
|
+
operands: {
|
|
398
|
+
path: largest.path,
|
|
399
|
+
numeratorEstimatedTokens: largest.estimate.estimatedTokens,
|
|
400
|
+
denominatorEstimatedTokens: total,
|
|
401
|
+
},
|
|
402
|
+
...(hasReference ? { reference: LARGEST_REFERENCE } : {}),
|
|
403
|
+
method: 'utf8-bytes-divisor/v1',
|
|
404
|
+
detail: `${largest.path} contributes ${largest.estimate.estimatedTokens}/${total} estimated_tokens (${largestFraction})`,
|
|
405
|
+
},
|
|
406
|
+
];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const ARTIFACT_STEP = new Map<string, number>([
|
|
410
|
+
['00_complexity_assessment.md', 0],
|
|
411
|
+
['01_requirements.md', 1],
|
|
412
|
+
['02_research.md', 2],
|
|
413
|
+
['03_adr/', 3],
|
|
414
|
+
['03.5_ideation_report.md', 3.5],
|
|
415
|
+
['04_domain_model.md', 4],
|
|
416
|
+
['05_architecture.md', 5],
|
|
417
|
+
['06_implementation_plan.md', 6],
|
|
418
|
+
['07_code_changes/change_manifest.md', 7],
|
|
419
|
+
['08_qe_report.md', 8],
|
|
420
|
+
['09_fleet_qe_assessment.md', 9],
|
|
421
|
+
['README.md', 8],
|
|
422
|
+
]);
|
|
423
|
+
|
|
424
|
+
const STEP_ARTIFACT = new Map<string, string[]>([
|
|
425
|
+
['0', ['00_complexity_assessment.md']],
|
|
426
|
+
['1', ['01_requirements.md']],
|
|
427
|
+
['2', ['02_research.md']],
|
|
428
|
+
['3', ['03_adr/']],
|
|
429
|
+
['3.5', ['03.5_ideation_report.md']],
|
|
430
|
+
['4', ['04_domain_model.md']],
|
|
431
|
+
['5', ['05_architecture.md']],
|
|
432
|
+
['6', ['06_implementation_plan.md']],
|
|
433
|
+
['7', ['07_code_changes/change_manifest.md']],
|
|
434
|
+
['8', ['08_qe_report.md', 'README.md']],
|
|
435
|
+
['9', ['09_fleet_qe_assessment.md']],
|
|
436
|
+
]);
|
|
437
|
+
|
|
438
|
+
function normalizedStep(value: unknown): string | undefined {
|
|
439
|
+
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
|
440
|
+
if (typeof value === 'string' && /^(?:[0-9]|10)(?:\.5)?$/.test(value.trim())) return value.trim();
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function expectedFeatureArtifacts(tier: FeatureTier, activeSteps: readonly (number | string)[]): readonly string[] {
|
|
445
|
+
const expected = new Set<string>([
|
|
446
|
+
'00_complexity_assessment.md',
|
|
447
|
+
'01_requirements.md',
|
|
448
|
+
'06_implementation_plan.md',
|
|
449
|
+
'07_code_changes/change_manifest.md',
|
|
450
|
+
'08_qe_report.md',
|
|
451
|
+
'README.md',
|
|
452
|
+
]);
|
|
453
|
+
if (tier === 'M' || tier === 'L' || tier === 'XL') {
|
|
454
|
+
expected.add('03_adr/');
|
|
455
|
+
expected.add('03.5_ideation_report.md');
|
|
456
|
+
expected.add('05_architecture.md');
|
|
457
|
+
}
|
|
458
|
+
if (tier === 'L' || tier === 'XL') {
|
|
459
|
+
expected.add('02_research.md');
|
|
460
|
+
expected.add('04_domain_model.md');
|
|
461
|
+
expected.add('09_fleet_qe_assessment.md');
|
|
462
|
+
}
|
|
463
|
+
for (const raw of activeSteps) {
|
|
464
|
+
const step = normalizedStep(raw);
|
|
465
|
+
if (step === undefined) continue;
|
|
466
|
+
for (const artifact of STEP_ARTIFACT.get(step) ?? []) expected.add(artifact);
|
|
467
|
+
}
|
|
468
|
+
return [...expected].sort();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function isArtifactContentPath(path: string): boolean {
|
|
472
|
+
return /^(?:0[0-9](?:[_.-][^/]*)?\.md)$/.test(path)
|
|
473
|
+
|| /^03_adr\/.+$/.test(path)
|
|
474
|
+
|| /^07_code_changes\/.+$/.test(path);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function isExpectedPresent(expected: string, present: ReadonlySet<string>): boolean {
|
|
478
|
+
if (expected.endsWith('/')) return [...present].some((path) => path.startsWith(expected));
|
|
479
|
+
return present.has(expected);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function dueArtifacts(
|
|
483
|
+
expected: readonly string[],
|
|
484
|
+
lifecycle: FeatureVolumeFact['lifecycle'],
|
|
485
|
+
): readonly string[] | undefined {
|
|
486
|
+
if (lifecycle === undefined || lifecycle === null || typeof lifecycle !== 'object') return undefined;
|
|
487
|
+
if (lifecycle.phase === 'complete') return expected;
|
|
488
|
+
const through = lifecycle.completedThroughStep;
|
|
489
|
+
if (typeof through !== 'number' || !Number.isFinite(through) || through < 0 || through > 10) return undefined;
|
|
490
|
+
return expected.filter((artifact) => (ARTIFACT_STEP.get(artifact) ?? Number.POSITIVE_INFINITY) <= through);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function validTier(value: unknown): value is FeatureTier {
|
|
494
|
+
return value === 'S' || value === 'M' || value === 'L' || value === 'XL';
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function evaluateFeature(raw: unknown): GuardObservation[] {
|
|
498
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
499
|
+
return unknownFeature('unknown-feature', 'malformed-feature-fact');
|
|
500
|
+
}
|
|
501
|
+
const feature = raw as Partial<FeatureVolumeFact>;
|
|
502
|
+
const slugPath = safeRelativePath(feature.slug);
|
|
503
|
+
const scope = slugPath !== undefined && !slugPath.includes('/') ? slugPath : 'unknown-feature';
|
|
504
|
+
if (feature.collection?.complete === false) {
|
|
505
|
+
return unknownFeature(scope, boundedDetail(feature.collection.reason ?? 'incomplete-feature-scan'), feature.collection.detail);
|
|
506
|
+
}
|
|
507
|
+
if (!Array.isArray(feature.artifacts)) return unknownFeature(scope, 'malformed-artifact-files');
|
|
508
|
+
|
|
509
|
+
let artifactBytes = 0;
|
|
510
|
+
const present = new Set<string>();
|
|
511
|
+
for (const rawArtifact of feature.artifacts) {
|
|
512
|
+
if (!rawArtifact || typeof rawArtifact !== 'object' || Array.isArray(rawArtifact)) {
|
|
513
|
+
return unknownFeature(scope, 'malformed-artifact-fact');
|
|
514
|
+
}
|
|
515
|
+
const artifact = rawArtifact as Partial<FeatureArtifactFact>;
|
|
516
|
+
const path = safeRelativePath(artifact.path);
|
|
517
|
+
if (path === undefined) return unknownFeature(scope, 'unsafe-artifact-path');
|
|
518
|
+
if (present.has(path)) return unknownFeature(scope, 'duplicate-artifact-path', path);
|
|
519
|
+
present.add(path);
|
|
520
|
+
if (!isArtifactContentPath(path)) continue;
|
|
521
|
+
if (!validByteCount(artifact.bytes)) return unknownFeature(scope, 'invalid-artifact-byte-count', path);
|
|
522
|
+
artifactBytes += artifact.bytes;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const ratioObservation = evaluateArtifactRatio(scope, artifactBytes, feature.diff);
|
|
526
|
+
const artifactObservation = evaluateArtifactSet(feature, scope, present);
|
|
527
|
+
return [ratioObservation, artifactObservation];
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function evaluateArtifactRatio(
|
|
531
|
+
scope: string,
|
|
532
|
+
artifactBytes: number,
|
|
533
|
+
diff: FeatureVolumeFact['diff'],
|
|
534
|
+
): GuardObservation {
|
|
535
|
+
if (!validByteCount(artifactBytes)) {
|
|
536
|
+
return unknownObservation(
|
|
537
|
+
'feature-artifact-diff-ratio',
|
|
538
|
+
'feature-artifact-diff-ratio',
|
|
539
|
+
scope,
|
|
540
|
+
'byte_ratio',
|
|
541
|
+
'empty-artifact-scope',
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
if (!diff || typeof diff !== 'object' || diff.attributable !== true) {
|
|
545
|
+
return unknownObservation(
|
|
546
|
+
'feature-artifact-diff-ratio',
|
|
547
|
+
'feature-artifact-diff-ratio',
|
|
548
|
+
scope,
|
|
549
|
+
'byte_ratio',
|
|
550
|
+
boundedDetail(diff?.reason ?? 'unattributable-diff'),
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
if (!validByteCount(diff.bytes)) {
|
|
554
|
+
return unknownObservation(
|
|
555
|
+
'feature-artifact-diff-ratio',
|
|
556
|
+
'feature-artifact-diff-ratio',
|
|
557
|
+
scope,
|
|
558
|
+
'byte_ratio',
|
|
559
|
+
diff.bytes === 0 ? 'zero-diff-denominator' : 'invalid-diff-denominator',
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
const ratio = artifactBytes / diff.bytes;
|
|
563
|
+
const signal = ratio < ARTIFACT_RATIO_LOW || ratio > ARTIFACT_RATIO_HIGH;
|
|
564
|
+
return {
|
|
565
|
+
schemaVersion: VOLUME_SHADOW_SCHEMA_VERSION,
|
|
566
|
+
rule: 'feature-artifact-diff-ratio',
|
|
567
|
+
metric: 'feature-artifact-diff-ratio',
|
|
568
|
+
scope,
|
|
569
|
+
status: signal ? 'outside-reference' : 'within-reference',
|
|
570
|
+
value: ratio,
|
|
571
|
+
unit: 'byte_ratio',
|
|
572
|
+
signal,
|
|
573
|
+
operands: {
|
|
574
|
+
artifactBytes,
|
|
575
|
+
unifiedDiffBytes: diff.bytes,
|
|
576
|
+
base: boundedDetail(diff.base ?? 'working-tree-base'),
|
|
577
|
+
head: boundedDetail(diff.head ?? 'working-tree'),
|
|
578
|
+
excludedFeaturePath: boundedDetail(diff.excludedFeaturePath ?? `features/${scope}/`),
|
|
579
|
+
},
|
|
580
|
+
reference: RATIO_REFERENCE,
|
|
581
|
+
method: boundedDetail(diff.method ?? 'git-unified-diff-bytes/v1'),
|
|
582
|
+
detail: `${artifactBytes}/${diff.bytes} artifact-to-unified-diff bytes = ${ratio}; unified-diff bytes are a proxy`,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function evaluateArtifactSet(
|
|
587
|
+
feature: Partial<FeatureVolumeFact>,
|
|
588
|
+
scope: string,
|
|
589
|
+
present: ReadonlySet<string>,
|
|
590
|
+
): GuardObservation {
|
|
591
|
+
if (!validTier(feature.tier)) {
|
|
592
|
+
return unknownObservation(
|
|
593
|
+
'feature-tier-artifact-set',
|
|
594
|
+
'feature-tier-artifacts',
|
|
595
|
+
scope,
|
|
596
|
+
'artifact_set',
|
|
597
|
+
'tier-unavailable',
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
if (!Array.isArray(feature.activeSteps)) {
|
|
601
|
+
return unknownObservation(
|
|
602
|
+
'feature-tier-artifact-set',
|
|
603
|
+
'feature-tier-artifacts',
|
|
604
|
+
scope,
|
|
605
|
+
'artifact_set',
|
|
606
|
+
'active-steps-unavailable',
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
if (feature.activeSteps.some((step) => normalizedStep(step) === undefined)) {
|
|
610
|
+
return unknownObservation(
|
|
611
|
+
'feature-tier-artifact-set',
|
|
612
|
+
'feature-tier-artifacts',
|
|
613
|
+
scope,
|
|
614
|
+
'artifact_set',
|
|
615
|
+
'active-steps-malformed',
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
const expected = new Set(expectedFeatureArtifacts(feature.tier, feature.activeSteps));
|
|
619
|
+
if (feature.namedConsumers !== undefined) {
|
|
620
|
+
if (!Array.isArray(feature.namedConsumers)) {
|
|
621
|
+
return unknownObservation(
|
|
622
|
+
'feature-tier-artifact-set',
|
|
623
|
+
'feature-tier-artifacts',
|
|
624
|
+
scope,
|
|
625
|
+
'artifact_set',
|
|
626
|
+
'consumer-artifacts-malformed',
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
for (const raw of feature.namedConsumers) {
|
|
630
|
+
const artifact = safeRelativePath(raw);
|
|
631
|
+
if (artifact === undefined || !ARTIFACT_STEP.has(artifact)) {
|
|
632
|
+
return unknownObservation(
|
|
633
|
+
'feature-tier-artifact-set',
|
|
634
|
+
'feature-tier-artifacts',
|
|
635
|
+
scope,
|
|
636
|
+
'artifact_set',
|
|
637
|
+
'consumer-artifact-outside-contract',
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
expected.add(artifact);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const due = dueArtifacts([...expected].sort(), feature.lifecycle);
|
|
644
|
+
if (due === undefined) {
|
|
645
|
+
return unknownObservation(
|
|
646
|
+
'feature-tier-artifact-set',
|
|
647
|
+
'feature-tier-artifacts',
|
|
648
|
+
scope,
|
|
649
|
+
'artifact_set',
|
|
650
|
+
'lifecycle-unavailable',
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
const missing = due.filter((artifact) => !isExpectedPresent(artifact, present));
|
|
654
|
+
const signal = missing.length > 0;
|
|
655
|
+
return {
|
|
656
|
+
schemaVersion: VOLUME_SHADOW_SCHEMA_VERSION,
|
|
657
|
+
rule: 'feature-tier-artifact-set',
|
|
658
|
+
metric: 'feature-tier-artifacts',
|
|
659
|
+
scope,
|
|
660
|
+
status: signal ? 'outside-reference' : 'within-reference',
|
|
661
|
+
value: missing,
|
|
662
|
+
unit: 'artifact_set',
|
|
663
|
+
signal,
|
|
664
|
+
operands: {
|
|
665
|
+
tier: feature.tier,
|
|
666
|
+
expectedDue: due,
|
|
667
|
+
present: [...present].sort(),
|
|
668
|
+
missing,
|
|
669
|
+
lifecycle: feature.lifecycle?.phase ?? 'unknown',
|
|
670
|
+
},
|
|
671
|
+
reference: {
|
|
672
|
+
kind: 'pipeline-contract',
|
|
673
|
+
source: '.agents/skills/feature-adr/SKILL.md#Step-Activation-Matrix',
|
|
674
|
+
expected: due,
|
|
675
|
+
},
|
|
676
|
+
method: 'feature-tier-active-steps/v1',
|
|
677
|
+
detail: signal
|
|
678
|
+
? `${feature.tier} tier is missing ${missing.join(', ')} that are due in the recorded lifecycle`
|
|
679
|
+
: `${feature.tier} tier has every artifact due in the recorded lifecycle`,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function finalize(observations: GuardObservation[]): VolumeShadowResult {
|
|
684
|
+
observations.sort((a, b) =>
|
|
685
|
+
(RULE_ORDER.get(a.rule) ?? 99) - (RULE_ORDER.get(b.rule) ?? 99)
|
|
686
|
+
|| a.scope.localeCompare(b.scope)
|
|
687
|
+
|| a.metric.localeCompare(b.metric));
|
|
688
|
+
const signals: VolumeShadowSignal[] = observations
|
|
689
|
+
.filter((observation) => observation.signal)
|
|
690
|
+
.map((observation) => ({
|
|
691
|
+
rule: observation.rule,
|
|
692
|
+
detail: `${observation.scope}: ${observation.detail}; SOFT shadow observation only`,
|
|
693
|
+
}));
|
|
694
|
+
const notes = observations
|
|
695
|
+
.filter((observation) => observation.status === 'unknown')
|
|
696
|
+
.map((observation) => `${observation.rule} ${observation.scope}: ${observation.detail}`);
|
|
697
|
+
return { observations, signals, notes };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export function evaluateVolumeShadow(input: unknown): VolumeShadowResult {
|
|
701
|
+
if (input === undefined) return EMPTY_RESULT;
|
|
702
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
703
|
+
return unknownVolumeShadow('malformed-volume-input');
|
|
704
|
+
}
|
|
705
|
+
const raw = input as Partial<VolumeShadowInput>;
|
|
706
|
+
if (raw.templates !== undefined && !Array.isArray(raw.templates)) {
|
|
707
|
+
return unknownVolumeShadow('malformed-template-targets');
|
|
708
|
+
}
|
|
709
|
+
if (raw.features !== undefined && !Array.isArray(raw.features)) {
|
|
710
|
+
return unknownVolumeShadow('malformed-feature-targets');
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const observations: GuardObservation[] = [];
|
|
714
|
+
const templateCounts = new Map<string, number>();
|
|
715
|
+
for (const target of raw.templates ?? []) {
|
|
716
|
+
const name = target && typeof target === 'object' && typeof (target as { target?: unknown }).target === 'string'
|
|
717
|
+
? (target as { target: string }).target
|
|
718
|
+
: 'unknown-target';
|
|
719
|
+
templateCounts.set(name, (templateCounts.get(name) ?? 0) + 1);
|
|
720
|
+
}
|
|
721
|
+
const seenTemplates = new Set<string>();
|
|
722
|
+
for (const target of raw.templates ?? []) {
|
|
723
|
+
const name = target && typeof target === 'object' && typeof (target as { target?: unknown }).target === 'string'
|
|
724
|
+
? (target as { target: string }).target
|
|
725
|
+
: 'unknown-target';
|
|
726
|
+
if (seenTemplates.has(name)) continue;
|
|
727
|
+
seenTemplates.add(name);
|
|
728
|
+
observations.push(...((templateCounts.get(name) ?? 0) > 1
|
|
729
|
+
? unknownTemplateTarget(boundedDetail(name), 'duplicate-template-target')
|
|
730
|
+
: evaluateTemplateTarget(target)));
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const featureCounts = new Map<string, number>();
|
|
734
|
+
for (const feature of raw.features ?? []) {
|
|
735
|
+
const slug = feature && typeof feature === 'object' && typeof (feature as { slug?: unknown }).slug === 'string'
|
|
736
|
+
? (feature as { slug: string }).slug
|
|
737
|
+
: 'unknown-feature';
|
|
738
|
+
featureCounts.set(slug, (featureCounts.get(slug) ?? 0) + 1);
|
|
739
|
+
}
|
|
740
|
+
const seenFeatures = new Set<string>();
|
|
741
|
+
for (const feature of raw.features ?? []) {
|
|
742
|
+
const slug = feature && typeof feature === 'object' && typeof (feature as { slug?: unknown }).slug === 'string'
|
|
743
|
+
? (feature as { slug: string }).slug
|
|
744
|
+
: 'unknown-feature';
|
|
745
|
+
if (seenFeatures.has(slug)) continue;
|
|
746
|
+
seenFeatures.add(slug);
|
|
747
|
+
observations.push(...((featureCounts.get(slug) ?? 0) > 1
|
|
748
|
+
? unknownFeature(boundedDetail(slug), 'duplicate-feature-target')
|
|
749
|
+
: evaluateFeature(feature)));
|
|
750
|
+
}
|
|
751
|
+
return finalize(observations);
|
|
752
|
+
}
|