@afokapu/atdd-bun 0.6.2 → 0.7.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/README.md +2 -0
- package/conventions/coder.bun/coder.bun.telemetry-forbidden-properties.convention.yaml +51 -0
- package/conventions/coder.bun/coder.bun.telemetry-implementation-binding.convention.yaml +45 -0
- package/conventions/coder.bun/coder.bun.telemetry-raw-string-emit.convention.yaml +53 -0
- package/conventions/coder.bun/coder.bun.telemetry-source-binding.convention.yaml +48 -0
- package/conventions/coder.bun/coder.bun.telemetry-vendor-sdk.convention.yaml +51 -0
- package/conventions/planner.telemetry/planner.telemetry.acceptance-decision.convention.yaml +58 -0
- package/conventions/planner.telemetry/planner.telemetry.logical-ownership.convention.yaml +50 -0
- package/conventions/planner.telemetry/planner.telemetry.metric-cardinality.convention.yaml +47 -0
- package/conventions/planner.telemetry/planner.telemetry.tracking-plan-schema.convention.yaml +75 -0
- package/conventions/tester.bun/tester.bun.telemetry-captured-sink.convention.yaml +47 -0
- package/conventions/tester.bun/tester.bun.telemetry-identity-assertion.convention.yaml +44 -0
- package/conventions/tester.bun/tester.bun.telemetry-required-item-coverage.convention.yaml +44 -0
- package/conventions/tester.bun/tester.bun.telemetry-test-binding.convention.yaml +52 -0
- package/conventions/tester.bun/tester.bun.telemetry-timing-semantics.convention.yaml +53 -0
- package/detectors/bun_telemetry_code/atdd.implementation.yaml +24 -0
- package/detectors/bun_telemetry_code/calls.mjs +104 -0
- package/detectors/bun_telemetry_code/checks/t_forbidden_properties.mjs +46 -0
- package/detectors/bun_telemetry_code/checks/t_implementation_binding.mjs +41 -0
- package/detectors/bun_telemetry_code/checks/t_raw_string_emit.mjs +34 -0
- package/detectors/bun_telemetry_code/checks/t_source_binding.mjs +48 -0
- package/detectors/bun_telemetry_code/checks/t_vendor_sdk.mjs +38 -0
- package/detectors/bun_telemetry_code/detect.mjs +50 -0
- package/detectors/bun_telemetry_code/fixtures/clean/plan/commons/E001.yaml +10 -0
- package/detectors/bun_telemetry_code/fixtures/clean/plan/commons/E002.yaml +7 -0
- package/detectors/bun_telemetry_code/fixtures/clean/plan/commons/_commons.yaml +6 -0
- package/detectors/bun_telemetry_code/fixtures/clean/src/wagons/commons/features/ingress/domain/accept-response.ts +8 -0
- package/detectors/bun_telemetry_code/fixtures/clean/src/wagons/commons/features/ingress/infrastructure/otel-adapter.ts +8 -0
- package/detectors/bun_telemetry_code/fixtures/clean/telemetry/commons/response-invocation-accepted/event.be.json +16 -0
- package/detectors/bun_telemetry_code/fixtures/clean/telemetry/commons/response-invocation-accepted/metric.be.duration.json +19 -0
- package/detectors/bun_telemetry_code/fixtures/clean/tests/wagons/commons/features/ingress/unit/accept-response.telemetry.test.ts +13 -0
- package/detectors/bun_telemetry_code/fixtures/clean/tests/wagons/commons/features/ingress/unit/accept-response.test.ts +9 -0
- package/detectors/bun_telemetry_code/fixtures/dirty/plan/commons/E001.yaml +8 -0
- package/detectors/bun_telemetry_code/fixtures/dirty/plan/commons/_commons.yaml +6 -0
- package/detectors/bun_telemetry_code/fixtures/dirty/src/wagons/commons/features/ingress/domain/accept-response.ts +13 -0
- package/detectors/bun_telemetry_code/fixtures/dirty/src/wagons/commons/features/ingress/domain/tracing.ts +3 -0
- package/detectors/bun_telemetry_code/fixtures/dirty/telemetry/commons/response-invocation-accepted/event.be.json +16 -0
- package/detectors/bun_telemetry_code/fixtures/dirty/tests/wagons/commons/features/ingress/unit/probe.test.ts +7 -0
- package/detectors/bun_telemetry_code/registry.mjs +34 -0
- package/detectors/bun_telemetry_test/_shared.mjs +92 -0
- package/detectors/bun_telemetry_test/atdd.implementation.yaml +24 -0
- package/detectors/bun_telemetry_test/checks/t_captured_sink.mjs +35 -0
- package/detectors/bun_telemetry_test/checks/t_identity_assertion.mjs +45 -0
- package/detectors/bun_telemetry_test/checks/t_required_item_coverage.mjs +37 -0
- package/detectors/bun_telemetry_test/checks/t_test_binding.mjs +56 -0
- package/detectors/bun_telemetry_test/checks/t_timing_semantics.mjs +51 -0
- package/detectors/bun_telemetry_test/detect.mjs +50 -0
- package/detectors/bun_telemetry_test/fixtures/clean/plan/commons/E001.yaml +10 -0
- package/detectors/bun_telemetry_test/fixtures/clean/plan/commons/E002.yaml +7 -0
- package/detectors/bun_telemetry_test/fixtures/clean/plan/commons/_commons.yaml +6 -0
- package/detectors/bun_telemetry_test/fixtures/clean/src/wagons/commons/features/ingress/domain/accept-response.ts +8 -0
- package/detectors/bun_telemetry_test/fixtures/clean/telemetry/commons/response-invocation-accepted/event.be.json +16 -0
- package/detectors/bun_telemetry_test/fixtures/clean/telemetry/commons/response-invocation-accepted/metric.be.duration.json +19 -0
- package/detectors/bun_telemetry_test/fixtures/clean/tests/wagons/commons/features/ingress/unit/accept-response.telemetry.test.ts +13 -0
- package/detectors/bun_telemetry_test/fixtures/clean/tests/wagons/commons/features/ingress/unit/accept-response.test.ts +7 -0
- package/detectors/bun_telemetry_test/fixtures/dirty/plan/commons/E001.yaml +10 -0
- package/detectors/bun_telemetry_test/fixtures/dirty/plan/commons/E002.yaml +7 -0
- package/detectors/bun_telemetry_test/fixtures/dirty/plan/commons/_commons.yaml +6 -0
- package/detectors/bun_telemetry_test/fixtures/dirty/src/wagons/commons/features/ingress/domain/accept-response.ts +8 -0
- package/detectors/bun_telemetry_test/fixtures/dirty/telemetry/commons/response-invocation-accepted/event.be.json +16 -0
- package/detectors/bun_telemetry_test/fixtures/dirty/telemetry/commons/response-invocation-accepted/metric.be.duration.json +19 -0
- package/detectors/bun_telemetry_test/fixtures/dirty/tests/wagons/commons/features/ingress/unit/dangling.telemetry.test.ts +9 -0
- package/detectors/bun_telemetry_test/fixtures/dirty/tests/wagons/commons/features/ingress/unit/loose.telemetry.test.ts +11 -0
- package/detectors/bun_telemetry_test/fixtures/dirty/tests/wagons/commons/features/ingress/unit/unbound.telemetry.test.ts +10 -0
- package/detectors/planner_telemetry_plan/atdd.implementation.yaml +22 -0
- package/detectors/planner_telemetry_plan/detect.mjs +9 -0
- package/detectors/planner_telemetry_plan/fixtures/clean/plan/commons/E001.yaml +10 -0
- package/detectors/planner_telemetry_plan/fixtures/clean/plan/commons/E002.yaml +7 -0
- package/detectors/planner_telemetry_plan/fixtures/clean/plan/commons/_commons.yaml +6 -0
- package/detectors/planner_telemetry_plan/fixtures/clean/src/wagons/commons/features/ingress/domain/accept-response.ts +8 -0
- package/detectors/planner_telemetry_plan/fixtures/clean/telemetry/commons/response-invocation-accepted/event.be.json +16 -0
- package/detectors/planner_telemetry_plan/fixtures/clean/telemetry/commons/response-invocation-accepted/metric.be.duration.json +19 -0
- package/detectors/planner_telemetry_plan/fixtures/clean/tests/wagons/commons/features/ingress/unit/accept-response.telemetry.test.ts +13 -0
- package/detectors/planner_telemetry_plan/fixtures/dirty/plan/commons/E001.yaml +9 -0
- package/detectors/planner_telemetry_plan/fixtures/dirty/plan/commons/E002.yaml +4 -0
- package/detectors/planner_telemetry_plan/fixtures/dirty/plan/commons/E003.yaml +7 -0
- package/detectors/planner_telemetry_plan/fixtures/dirty/plan/commons/_commons.yaml +6 -0
- package/detectors/planner_telemetry_plan/fixtures/dirty/telemetry/commons/extra.json +1 -0
- package/detectors/planner_telemetry_plan/fixtures/dirty/telemetry/commons/orphan-artifact/event.be.json +14 -0
- package/detectors/planner_telemetry_plan/fixtures/dirty/telemetry/commons/response-invocation-accepted/event.be.json +16 -0
- package/detectors/planner_telemetry_plan/fixtures/dirty/telemetry/commons/response-invocation-accepted/metric.be.duration.json +22 -0
- package/detectors/planner_telemetry_plan/fixtures/dirty/telemetry/commons/response-invocation-accepted/notes.json +1 -0
- package/integrity.json +91 -8
- package/lib/scan.mjs +37 -0
- package/package.json +1 -1
- package/planner-schemas/acceptance.schema.json +70 -1
- package/planner-schemas/telemetry-plan.schema.json +133 -0
- package/relationships.yaml +174 -0
- package/src/enforce.ts +2 -1
- package/src/index.ts +2 -0
- package/src/telemetry-plan.ts +223 -0
- package/src/topology.ts +3 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Telemetry Tracking-Plan Item Schema",
|
|
4
|
+
"description": "Validates one concrete telemetry tracking-plan file under telemetry/<theme>/<artifact>/{kind}.{plane}[.{measure}].json. Local shape only: path mirroring, kind/plane/measure coherence with the file name, wagon ownership resolution and acceptance linkage are cross-artifact validator concerns (src/telemetry-plan.ts).",
|
|
5
|
+
"$comment": "Shipped by the telemetry profile (planner_telemetry_plan). The capability is deliberately inert until a repository adopts it: no telemetry/ root and no acceptance telemetry declaration means no findings. `signal` in acceptance.schema.json keeps the executable bar (signal.metric + signal.threshold); the descriptive arrays it also carries are superseded by the acceptance `telemetry` block and by this registry, and are not renamed here — that migration is a separate, documented step.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["id", "version", "logical_artifact", "kind", "plane", "owner", "purpose", "acceptances", "properties"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"id": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"pattern": "^telemetry:(event|trace|metric|log):(ui|ux|be|nw|db|st|tm|sc|au|fn|if):[a-z][a-z0-9-]*:[a-z][a-z0-9-]*(?::(latency|duration|throughput|error_rate|success_rate|count|size|age|staleness|freshness))?$",
|
|
13
|
+
"description": "Concrete telemetry URN: telemetry:{kind}:{plane}:{theme}:{artifact}[:{measure}]. Must mirror the file's path and name; the validator checks that coherence."
|
|
14
|
+
},
|
|
15
|
+
"version": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
|
|
18
|
+
"description": "SemVer core for this item's contract. Changing properties, required or forbidden_properties is a contract change and must bump it."
|
|
19
|
+
},
|
|
20
|
+
"logical_artifact": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"pattern": "^telemetry:[a-z][a-z0-9-]*:[a-z][a-z0-9-]*$",
|
|
23
|
+
"description": "The stable architectural identity of the observable outcome: telemetry:{theme}:{artifact}. Not an exporter event. Must be owned by exactly one wagon's produce[] telemetry entry."
|
|
24
|
+
},
|
|
25
|
+
"kind": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": ["event", "trace", "metric", "log"]
|
|
28
|
+
},
|
|
29
|
+
"plane": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"enum": ["ui", "ux", "be", "nw", "db", "st", "tm", "sc", "au", "fn", "if"]
|
|
32
|
+
},
|
|
33
|
+
"measure": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"enum": ["latency", "duration", "throughput", "error_rate", "success_rate", "count", "size", "age", "staleness", "freshness"],
|
|
36
|
+
"description": "Required for metric items (and only metric items); must match the measure segment of the file name and of the id."
|
|
37
|
+
},
|
|
38
|
+
"instrument": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"enum": ["counter", "gauge", "histogram", "summary"],
|
|
41
|
+
"description": "Metric instrument type per the telemetry kind vocabulary."
|
|
42
|
+
},
|
|
43
|
+
"unit": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Unit of measure for metric items (ms, s, bytes, percent, count)."
|
|
46
|
+
},
|
|
47
|
+
"owner": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"pattern": "^[a-z][a-z0-9-]*$",
|
|
50
|
+
"description": "The wagon slug that owns the logical artifact. The validator checks it equals the wagon whose produce[] declares the logical artifact."
|
|
51
|
+
},
|
|
52
|
+
"purpose": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"minLength": 20,
|
|
55
|
+
"description": "The question this item answers. A tracking plan is read by people deciding what to alert and dashboard on; an item that cannot state its question is not planned."
|
|
56
|
+
},
|
|
57
|
+
"acceptances": {
|
|
58
|
+
"type": "array",
|
|
59
|
+
"items": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"pattern": "^acc:[A-Za-z0-9._:-]+$",
|
|
62
|
+
"description": "Acceptance URNs that require this item. Each must resolve to a declared acceptance in the plan."
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"properties": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"description": "The fields this item may carry, by name. `required` names must all appear here.",
|
|
68
|
+
"additionalProperties": {
|
|
69
|
+
"$ref": "#/definitions/property"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"required": {
|
|
73
|
+
"type": "array",
|
|
74
|
+
"items": {"type": "string", "minLength": 1},
|
|
75
|
+
"description": "Property names that must be present on every emission."
|
|
76
|
+
},
|
|
77
|
+
"forbidden_properties": {
|
|
78
|
+
"type": "array",
|
|
79
|
+
"items": {"type": "string", "minLength": 1},
|
|
80
|
+
"description": "Property names that must never be attached to this item (raw payloads, secrets, prompts)."
|
|
81
|
+
},
|
|
82
|
+
"dimensions": {
|
|
83
|
+
"type": "array",
|
|
84
|
+
"description": "Metric labels. Every dimension declares cardinality; high cardinality is forbidden for metrics — use an event, log or trace property instead.",
|
|
85
|
+
"items": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"required": ["name", "cardinality"],
|
|
88
|
+
"additionalProperties": false,
|
|
89
|
+
"properties": {
|
|
90
|
+
"name": {"type": "string", "minLength": 1},
|
|
91
|
+
"cardinality": {"type": "string", "enum": ["low", "medium", "high"]}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"timing": {
|
|
96
|
+
"type": "array",
|
|
97
|
+
"description": "Critical timing semantics a telemetry test bound to this item must exercise: post-commit (emitted only after the transaction commits), absent-after-rollback (never emitted when the transaction rolls back), correlation-across-async (correlation context continues across an async handoff). Declaring a semantic is what makes it testable; a test that cannot exercise it should not claim the item.",
|
|
98
|
+
"items": {"type": "string", "enum": ["post-commit", "absent-after-rollback", "correlation-across-async"]}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"definitions": {
|
|
102
|
+
"property": {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"required": ["type"],
|
|
105
|
+
"additionalProperties": false,
|
|
106
|
+
"properties": {
|
|
107
|
+
"type": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"enum": ["string", "integer", "number", "boolean"]
|
|
110
|
+
},
|
|
111
|
+
"classification": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"enum": ["internal", "external", "pii"],
|
|
114
|
+
"description": "Who may see the value. PII forces a justification nobody has to reconstruct later."
|
|
115
|
+
},
|
|
116
|
+
"cardinality": {
|
|
117
|
+
"type": "string",
|
|
118
|
+
"enum": ["low", "medium", "high"],
|
|
119
|
+
"description": "Distinct-value volume. High cardinality is legal in event, log and trace properties where justified; never as a metric label."
|
|
120
|
+
},
|
|
121
|
+
"allowed_values": {
|
|
122
|
+
"type": "array",
|
|
123
|
+
"minItems": 1,
|
|
124
|
+
"items": {"type": ["string", "number", "boolean"]},
|
|
125
|
+
"description": "Closed vocabulary for this property."
|
|
126
|
+
},
|
|
127
|
+
"description": {
|
|
128
|
+
"type": "string"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
package/relationships.yaml
CHANGED
|
@@ -81,6 +81,11 @@ nodes:
|
|
|
81
81
|
- coder.bun.security-sql-injection
|
|
82
82
|
- coder.bun.station-master-interlocking-routing
|
|
83
83
|
- coder.bun.station-master-journey-routing
|
|
84
|
+
- coder.bun.telemetry-forbidden-properties
|
|
85
|
+
- coder.bun.telemetry-implementation-binding
|
|
86
|
+
- coder.bun.telemetry-raw-string-emit
|
|
87
|
+
- coder.bun.telemetry-source-binding
|
|
88
|
+
- coder.bun.telemetry-vendor-sdk
|
|
84
89
|
- coder.bun.wagon-honours-its-contract
|
|
85
90
|
- coder.htmx.fragment-escapes-interpolation
|
|
86
91
|
- coder.htmx.swap-no-inline-handler
|
|
@@ -220,6 +225,10 @@ nodes:
|
|
|
220
225
|
- planner.steps.linguistic-patterns
|
|
221
226
|
- planner.steps.sequence
|
|
222
227
|
- planner.subject.invariants
|
|
228
|
+
- planner.telemetry.acceptance-decision
|
|
229
|
+
- planner.telemetry.logical-ownership
|
|
230
|
+
- planner.telemetry.metric-cardinality
|
|
231
|
+
- planner.telemetry.tracking-plan-schema
|
|
223
232
|
- planner.theme.definition
|
|
224
233
|
- planner.theme.must-be-canonical
|
|
225
234
|
- planner.theme.theme-zero-mandatory
|
|
@@ -310,7 +319,12 @@ nodes:
|
|
|
310
319
|
- tester.bun.security-input
|
|
311
320
|
- tester.bun.smoke-no-collaborator-substitution
|
|
312
321
|
- tester.bun.smoke-observable-assertion
|
|
322
|
+
- tester.bun.telemetry-captured-sink
|
|
313
323
|
- tester.bun.telemetry-emit
|
|
324
|
+
- tester.bun.telemetry-identity-assertion
|
|
325
|
+
- tester.bun.telemetry-required-item-coverage
|
|
326
|
+
- tester.bun.telemetry-test-binding
|
|
327
|
+
- tester.bun.telemetry-timing-semantics
|
|
314
328
|
- tester.bun.test-carries-urn-identity
|
|
315
329
|
- tester.bun.test-imports-bun-test
|
|
316
330
|
- tester.bun.test-isolation-no-live-state
|
|
@@ -5012,3 +5026,163 @@ edges:
|
|
|
5012
5026
|
target_ref: atdd-bun.planner.identity-required
|
|
5013
5027
|
type: enables
|
|
5014
5028
|
origin: "@afokapu/atdd-bun"
|
|
5029
|
+
- confidence: 1
|
|
5030
|
+
constraint: mandatory
|
|
5031
|
+
control: internal
|
|
5032
|
+
foundation: finish_to_start
|
|
5033
|
+
reason: the tracking-plan tree is the versioned realization of the telemetry URN-to-filesystem mapping
|
|
5034
|
+
source_ref: planner.telemetry.tracking-plan-schema
|
|
5035
|
+
strength: important
|
|
5036
|
+
target_ref: planner.wagon.telemetry-filesystem
|
|
5037
|
+
type: requires
|
|
5038
|
+
origin: "@afokapu/atdd-bun"
|
|
5039
|
+
- confidence: 1
|
|
5040
|
+
constraint: mandatory
|
|
5041
|
+
control: internal
|
|
5042
|
+
foundation: finish_to_start
|
|
5043
|
+
reason: "a tracking-plan item's path mirrors its concrete telemetry identity"
|
|
5044
|
+
source_ref: planner.telemetry.tracking-plan-schema
|
|
5045
|
+
strength: important
|
|
5046
|
+
target_ref: planner.artifact-naming.path-mirrors-identity
|
|
5047
|
+
type: follows
|
|
5048
|
+
origin: "@afokapu/atdd-bun"
|
|
5049
|
+
- confidence: 1
|
|
5050
|
+
constraint: mandatory
|
|
5051
|
+
control: internal
|
|
5052
|
+
foundation: finish_to_start
|
|
5053
|
+
reason: "wagon produce[] telemetry entries are the ownership declarations items resolve against"
|
|
5054
|
+
source_ref: planner.telemetry.logical-ownership
|
|
5055
|
+
strength: important
|
|
5056
|
+
target_ref: planner.wagon.produce-consume-artifacts
|
|
5057
|
+
type: requires
|
|
5058
|
+
origin: "@afokapu/atdd-bun"
|
|
5059
|
+
- confidence: 1
|
|
5060
|
+
constraint: mandatory
|
|
5061
|
+
control: internal
|
|
5062
|
+
foundation: finish_to_start
|
|
5063
|
+
reason: the disposition decision governs the telemetry URNs an acceptance may declare
|
|
5064
|
+
source_ref: planner.telemetry.acceptance-decision
|
|
5065
|
+
strength: important
|
|
5066
|
+
target_ref: planner.acceptance.signal-telemetry-naming
|
|
5067
|
+
type: requires
|
|
5068
|
+
origin: "@afokapu/atdd-bun"
|
|
5069
|
+
- confidence: 1
|
|
5070
|
+
constraint: mandatory
|
|
5071
|
+
control: internal
|
|
5072
|
+
foundation: finish_to_start
|
|
5073
|
+
reason: "metric measures and dimensions inherit the naming rule's cardinality discipline"
|
|
5074
|
+
source_ref: planner.telemetry.metric-cardinality
|
|
5075
|
+
strength: important
|
|
5076
|
+
target_ref: planner.acceptance.signal-telemetry-naming
|
|
5077
|
+
type: requires
|
|
5078
|
+
origin: "@afokapu/atdd-bun"
|
|
5079
|
+
- confidence: 1
|
|
5080
|
+
constraint: mandatory
|
|
5081
|
+
control: internal
|
|
5082
|
+
foundation: finish_to_start
|
|
5083
|
+
reason: "a source Telemetry: reference resolves against the tracking-plan registry"
|
|
5084
|
+
source_ref: coder.bun.telemetry-source-binding
|
|
5085
|
+
strength: important
|
|
5086
|
+
target_ref: planner.telemetry.tracking-plan-schema
|
|
5087
|
+
type: requires
|
|
5088
|
+
origin: "@afokapu/atdd-bun"
|
|
5089
|
+
- confidence: 1
|
|
5090
|
+
constraint: mandatory
|
|
5091
|
+
control: internal
|
|
5092
|
+
foundation: finish_to_start
|
|
5093
|
+
reason: only telemetry an acceptance has decided and the plan declares may be emitted by name
|
|
5094
|
+
source_ref: coder.bun.telemetry-raw-string-emit
|
|
5095
|
+
strength: important
|
|
5096
|
+
target_ref: planner.telemetry.acceptance-decision
|
|
5097
|
+
type: requires
|
|
5098
|
+
origin: "@afokapu/atdd-bun"
|
|
5099
|
+
- confidence: 1
|
|
5100
|
+
constraint: mandatory
|
|
5101
|
+
control: internal
|
|
5102
|
+
foundation: start_to_start
|
|
5103
|
+
reason: vendor SDKs stay in infrastructure adapters as framework imports stay out of domain
|
|
5104
|
+
source_ref: coder.bun.telemetry-vendor-sdk
|
|
5105
|
+
strength: important
|
|
5106
|
+
target_ref: coder.bun.commons-domain-no-framework-import
|
|
5107
|
+
type: runs_alongside
|
|
5108
|
+
origin: "@afokapu/atdd-bun"
|
|
5109
|
+
- confidence: 1
|
|
5110
|
+
constraint: mandatory
|
|
5111
|
+
control: internal
|
|
5112
|
+
foundation: finish_to_start
|
|
5113
|
+
reason: the forbidden-property list comes from the tracking-plan item
|
|
5114
|
+
source_ref: coder.bun.telemetry-forbidden-properties
|
|
5115
|
+
strength: important
|
|
5116
|
+
target_ref: planner.telemetry.tracking-plan-schema
|
|
5117
|
+
type: requires
|
|
5118
|
+
origin: "@afokapu/atdd-bun"
|
|
5119
|
+
- confidence: 1
|
|
5120
|
+
constraint: mandatory
|
|
5121
|
+
control: internal
|
|
5122
|
+
foundation: finish_to_start
|
|
5123
|
+
reason: the binding obligation runs on the same header the source rule resolves
|
|
5124
|
+
source_ref: coder.bun.telemetry-implementation-binding
|
|
5125
|
+
strength: important
|
|
5126
|
+
target_ref: coder.bun.telemetry-source-binding
|
|
5127
|
+
type: requires
|
|
5128
|
+
origin: "@afokapu/atdd-bun"
|
|
5129
|
+
- confidence: 1
|
|
5130
|
+
constraint: mandatory
|
|
5131
|
+
control: internal
|
|
5132
|
+
foundation: finish_to_start
|
|
5133
|
+
reason: required items come from acceptance decisions
|
|
5134
|
+
source_ref: coder.bun.telemetry-implementation-binding
|
|
5135
|
+
strength: important
|
|
5136
|
+
target_ref: planner.telemetry.acceptance-decision
|
|
5137
|
+
type: requires
|
|
5138
|
+
origin: "@afokapu/atdd-bun"
|
|
5139
|
+
- confidence: 1
|
|
5140
|
+
constraint: mandatory
|
|
5141
|
+
control: internal
|
|
5142
|
+
foundation: finish_to_start
|
|
5143
|
+
reason: a bound test must point at the acceptance whose decision requires the item
|
|
5144
|
+
source_ref: tester.bun.telemetry-test-binding
|
|
5145
|
+
strength: important
|
|
5146
|
+
target_ref: planner.telemetry.acceptance-decision
|
|
5147
|
+
type: requires
|
|
5148
|
+
origin: "@afokapu/atdd-bun"
|
|
5149
|
+
- confidence: 1
|
|
5150
|
+
constraint: mandatory
|
|
5151
|
+
control: internal
|
|
5152
|
+
foundation: finish_to_start
|
|
5153
|
+
reason: the captured-sink obligation carries the emission assertion into the telemetry profile
|
|
5154
|
+
source_ref: tester.bun.telemetry-captured-sink
|
|
5155
|
+
strength: important
|
|
5156
|
+
target_ref: tester.bun.telemetry-emit
|
|
5157
|
+
type: requires
|
|
5158
|
+
origin: "@afokapu/atdd-bun"
|
|
5159
|
+
- confidence: 1
|
|
5160
|
+
constraint: mandatory
|
|
5161
|
+
control: internal
|
|
5162
|
+
foundation: finish_to_start
|
|
5163
|
+
reason: exact identity extends the emission assertion from any-call to the named item
|
|
5164
|
+
source_ref: tester.bun.telemetry-identity-assertion
|
|
5165
|
+
strength: important
|
|
5166
|
+
target_ref: tester.bun.telemetry-emit
|
|
5167
|
+
type: requires
|
|
5168
|
+
origin: "@afokapu/atdd-bun"
|
|
5169
|
+
- confidence: 1
|
|
5170
|
+
constraint: mandatory
|
|
5171
|
+
control: internal
|
|
5172
|
+
foundation: finish_to_start
|
|
5173
|
+
reason: telemetry items follow the acceptance-has-test discipline
|
|
5174
|
+
source_ref: tester.bun.telemetry-required-item-coverage
|
|
5175
|
+
strength: important
|
|
5176
|
+
target_ref: traceability.plan.executable-acceptance-has-test
|
|
5177
|
+
type: requires
|
|
5178
|
+
origin: "@afokapu/atdd-bun"
|
|
5179
|
+
- confidence: 1
|
|
5180
|
+
constraint: mandatory
|
|
5181
|
+
control: internal
|
|
5182
|
+
foundation: finish_to_start
|
|
5183
|
+
reason: timing semantics are declared on the tracking-plan item
|
|
5184
|
+
source_ref: tester.bun.telemetry-timing-semantics
|
|
5185
|
+
strength: important
|
|
5186
|
+
target_ref: planner.telemetry.tracking-plan-schema
|
|
5187
|
+
type: requires
|
|
5188
|
+
origin: "@afokapu/atdd-bun"
|
package/src/enforce.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { topologyFor } from "./topology";
|
|
6
6
|
|
|
7
|
-
export type Profile = "traceability" | "topology" | "docs" | "planner" | "coder" | "tester" | "security" | "architecture" | "metrics" | "runtime" | "interlocking" | "htmx" | "design" | "all";
|
|
7
|
+
export type Profile = "traceability" | "topology" | "docs" | "planner" | "telemetry" | "coder" | "tester" | "security" | "architecture" | "metrics" | "runtime" | "interlocking" | "htmx" | "design" | "all";
|
|
8
8
|
|
|
9
9
|
export type Violation = {
|
|
10
10
|
rule_id: string;
|
|
@@ -27,6 +27,7 @@ const profiles: Record<Exclude<Profile, "all">, string[]> = {
|
|
|
27
27
|
topology: ["atdd_topology"],
|
|
28
28
|
docs: ["planner_docs_capability"],
|
|
29
29
|
planner: ["planner_plan_integrity", "planner_schema_validation", "planner_static_validators", "atdd_topology"],
|
|
30
|
+
telemetry: ["planner_telemetry_plan", "bun_telemetry_code", "bun_telemetry_test"],
|
|
30
31
|
coder: ["bun_green_traceability_detector", "bun_clean_architecture_detector", "bun_ts_metrics_detector", "bun_fullstack_detector", "bun_design_system_detector", "bun_responsive_detector", "atdd_topology"],
|
|
31
32
|
tester: ["bun_tester_discipline_detector", "htmx_e2e_detector", "atdd_topology"],
|
|
32
33
|
security: ["bun_security_hygiene_detector"],
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type { DocumentationCheck, DocumentationDeclaration, DocumentationRender,
|
|
|
5
5
|
export { loadPlan, traceabilityPlan, validatePlan } from "./planner-kernel";
|
|
6
6
|
export type { PlanArtifact, PlanFinding, PlanGraph, PlanKind } from "./planner-kernel";
|
|
7
7
|
export { validateStaticPlannerConventions } from "./planner-validators";
|
|
8
|
+
export { validateTelemetryPlan, loadTelemetryFiles, telemetryDecisionOf, CONCRETE_URN } from "./telemetry-plan";
|
|
9
|
+
export type { TelemetryPlanItem, TelemetryFile } from "./telemetry-plan";
|
|
8
10
|
export { PLANNER_SCHEMA_RULE_ID, validatePlannerSchemas } from "./planner-schema-validator";
|
|
9
11
|
export { defaultHookPolicy, hookEvents, hooksStatus, installHooks, runHook, uninstallHooks } from "./hooks";
|
|
10
12
|
export type { HookEvent, HookPolicy } from "./hooks";
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import Ajv, { type ValidateFunction } from "ajv";
|
|
2
|
+
import addFormats from "ajv-formats";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
5
|
+
import { join, relative, resolve } from "node:path";
|
|
6
|
+
import { loadPlan, type PlanFinding } from "./planner-kernel";
|
|
7
|
+
import { topologyFor } from "./topology";
|
|
8
|
+
|
|
9
|
+
/** The telemetry tracking-plan validator: the plan-side half of the telemetry profile.
|
|
10
|
+
*
|
|
11
|
+
* A tracking plan is the versioned source of truth for what a repository observes. The
|
|
12
|
+
* telemetry/ tree declares concrete items (event / trace / metric / log per plane); wagons
|
|
13
|
+
* own the logical artifacts they realize; every acceptance makes an explicit decision —
|
|
14
|
+
* `required` (with items that must resolve into the tree) or `not-applicable` (with a
|
|
15
|
+
* rationale). The capability is inert until adopted: no telemetry root and no acceptance
|
|
16
|
+
* telemetry declaration means no findings, so upgrading the package changes nothing for a
|
|
17
|
+
* repository that has not opted in. */
|
|
18
|
+
|
|
19
|
+
const KINDS = ["event", "trace", "metric", "log"] as const;
|
|
20
|
+
const PLANES = ["ui", "ux", "be", "nw", "db", "st", "tm", "sc", "au", "fn", "if"] as const;
|
|
21
|
+
const MEASURES = ["latency", "duration", "throughput", "error_rate", "success_rate", "count", "size", "age", "staleness", "freshness"] as const;
|
|
22
|
+
const SEGMENT = "[a-z][a-z0-9-]*";
|
|
23
|
+
export const CONCRETE_URN = new RegExp(`^telemetry:(?:${KINDS.join("|")}):(?:${PLANES.join("|")}):${SEGMENT}:${SEGMENT}(?::(?:${MEASURES.join("|")}))?$`);
|
|
24
|
+
const FILE_NAME = new RegExp(`^(event|trace|metric|log)\\.(${PLANES.join("|")})(?:\\.(${MEASURES.join("|")}))?\\.json$`);
|
|
25
|
+
const SEGMENT_PATTERN = new RegExp(`^${SEGMENT}$`);
|
|
26
|
+
const schemaFile = join(resolve(import.meta.dir, ".."), "planner-schemas", "telemetry-plan.schema.json");
|
|
27
|
+
|
|
28
|
+
export type TelemetryPlanItem = { file: string; data: Record<string, unknown> };
|
|
29
|
+
/** One file under the telemetry root: its place in the tree, its parsed body, or why it would not parse. */
|
|
30
|
+
export type TelemetryFile = { file: string; segments: string[]; data: Record<string, unknown> | null; error?: string };
|
|
31
|
+
|
|
32
|
+
const finding = (rule_id: string, file: string, evidence: string): PlanFinding => ({ rule_id, file, evidence });
|
|
33
|
+
const text = (value: unknown): string => (typeof value === "string" ? value : "");
|
|
34
|
+
const records = (value: unknown): Array<Record<string, unknown>> => (Array.isArray(value) ? value.filter(entry => entry && typeof entry === "object" && !Array.isArray(entry)) : []);
|
|
35
|
+
|
|
36
|
+
async function walk(root: string): Promise<string[]> {
|
|
37
|
+
if (!existsSync(root)) return [];
|
|
38
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
39
|
+
return (await Promise.all(entries.map(async entry => {
|
|
40
|
+
const path = join(root, entry.name);
|
|
41
|
+
// `_generated` trees are derived material, never authored plan items — same exclusion the
|
|
42
|
+
// plan kernel applies under plan/.
|
|
43
|
+
if (entry.isDirectory() && entry.name === "_generated") return [];
|
|
44
|
+
return entry.isDirectory() ? walk(path) : entry.isFile() && entry.name.endsWith(".json") ? [path] : [];
|
|
45
|
+
}))).flat().sort();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function itemValidator(): Promise<ValidateFunction> {
|
|
49
|
+
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true, strict: false });
|
|
50
|
+
addFormats(ajv);
|
|
51
|
+
return ajv.compile(JSON.parse(await readFile(schemaFile, "utf8")));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Wagon ownership of logical telemetry artifacts, from produce[].telemetry entries (string or list). */
|
|
55
|
+
function telemetryOwners(wagons: Array<{ id: string; data: Record<string, unknown> }>): Map<string, string[]> {
|
|
56
|
+
const owners = new Map<string, string[]>();
|
|
57
|
+
for (const wagon of wagons) {
|
|
58
|
+
const slug = text(wagon.data.wagon) || wagon.id.replace(/^wagon:/, "");
|
|
59
|
+
for (const entry of records(wagon.data.produce)) {
|
|
60
|
+
const declared = entry.telemetry;
|
|
61
|
+
for (const value of Array.isArray(declared) ? declared.map(text) : [text(declared)]) {
|
|
62
|
+
if (value) owners.set(value, [...(owners.get(value) ?? []), slug]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return owners;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The acceptance's telemetry decision: disposition plus the concrete URNs it lists, any shape. */
|
|
70
|
+
export function telemetryDecisionOf(acceptance: { id: string; data: Record<string, unknown> }): { disposition: string; urns: string[] } {
|
|
71
|
+
const decision = acceptance.data.telemetry;
|
|
72
|
+
if (!decision || typeof decision !== "object" || Array.isArray(decision)) return { disposition: "", urns: [] };
|
|
73
|
+
const record = decision as Record<string, unknown>;
|
|
74
|
+
return {
|
|
75
|
+
disposition: text(record.disposition),
|
|
76
|
+
urns: ["events", "metrics", "traces", "logs"].flatMap(key => (Array.isArray(record[key]) ? record[key] : []).map(text)).filter(Boolean),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Load the tracking plan: the adoption gate, every file under the telemetry root (parsed or
|
|
81
|
+
* not), and every acceptance's telemetry decision. Shared by the plan-side validator and both
|
|
82
|
+
* code- and test-side detectors, so all of them judge the same registry. */
|
|
83
|
+
export async function loadTelemetryFiles(root = process.cwd()): Promise<{ adopted: boolean; files: TelemetryFile[]; requiredIds: Set<string>; decisions: Map<string, { disposition: string; urns: string[]; file: string }> }> {
|
|
84
|
+
const absolute = resolve(root), topology = await topologyFor(absolute);
|
|
85
|
+
const telemetryRoot = join(absolute, topology.telemetryRoot);
|
|
86
|
+
const plan = await loadPlan(absolute);
|
|
87
|
+
const acceptances = plan.artifacts.filter(artifact => artifact.kind === "acceptance");
|
|
88
|
+
// Adoption is a positive act: create the telemetry root, or let any acceptance declare a telemetry
|
|
89
|
+
// decision. Until then the capability is inert — an upgraded package must not fail an unadopting repo.
|
|
90
|
+
const adopted = existsSync(telemetryRoot) || acceptances.some(acceptance => acceptance.data.telemetry !== undefined);
|
|
91
|
+
const decisions = new Map<string, { disposition: string; urns: string[]; file: string }>();
|
|
92
|
+
const requiredIds = new Set<string>();
|
|
93
|
+
for (const acceptance of acceptances) {
|
|
94
|
+
const decision = telemetryDecisionOf(acceptance);
|
|
95
|
+
decisions.set(acceptance.id, { ...decision, file: acceptance.file });
|
|
96
|
+
if (decision.disposition === "required") for (const urn of decision.urns) requiredIds.add(urn);
|
|
97
|
+
}
|
|
98
|
+
const files: TelemetryFile[] = [];
|
|
99
|
+
for (const path of await walk(telemetryRoot)) {
|
|
100
|
+
const file = relative(absolute, path).replaceAll("\\", "/");
|
|
101
|
+
const segments = relative(telemetryRoot, path).replaceAll("\\", "/").split("/");
|
|
102
|
+
try {
|
|
103
|
+
const data: unknown = JSON.parse(await readFile(path, "utf8"));
|
|
104
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) { files.push({ file, segments, data: null, error: "tracking-plan item must be a JSON object" }); continue; }
|
|
105
|
+
files.push({ file, segments, data: data as Record<string, unknown> });
|
|
106
|
+
} catch (error) {
|
|
107
|
+
files.push({ file, segments, data: null, error: `could not parse tracking-plan item: ${String(error)}` });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { adopted, files, requiredIds, decisions };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function validateTelemetryPlan(root = process.cwd()): Promise<PlanFinding[]> {
|
|
114
|
+
const absolute = resolve(root), topology = await topologyFor(absolute);
|
|
115
|
+
const { adopted, files } = await loadTelemetryFiles(absolute);
|
|
116
|
+
if (!adopted) return [];
|
|
117
|
+
|
|
118
|
+
const findings: PlanFinding[] = [], validate = await itemValidator(), items: TelemetryPlanItem[] = [], byId = new Map<string, TelemetryPlanItem>();
|
|
119
|
+
|
|
120
|
+
for (const entry of files) {
|
|
121
|
+
const { file, segments } = entry;
|
|
122
|
+
const name = segments.at(-1) ?? "", match = FILE_NAME.exec(name);
|
|
123
|
+
if (segments.length !== 3 || !match || !SEGMENT_PATTERN.test(segments[0]) || !SEGMENT_PATTERN.test(segments[1])) {
|
|
124
|
+
findings.push(finding("planner.telemetry.tracking-plan-schema", file, `tracking-plan files live at ${topology.telemetryRoot}/<theme>/<artifact>/{event|trace|metric|log}.<plane>[.<measure>].json; found ${file}`));
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!entry.data) {
|
|
128
|
+
findings.push(finding("planner.telemetry.tracking-plan-schema", file, entry.error ?? "tracking-plan item must be a JSON object"));
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const record = entry.data;
|
|
132
|
+
if (!validate(record)) {
|
|
133
|
+
for (const error of validate.errors ?? []) findings.push(finding(
|
|
134
|
+
"planner.telemetry.tracking-plan-schema", file,
|
|
135
|
+
`${file} violates telemetry-plan.schema.json: ${error.instancePath || "/"} ${error.message ?? error.keyword}`,
|
|
136
|
+
));
|
|
137
|
+
}
|
|
138
|
+
items.push({ file, data: record });
|
|
139
|
+
|
|
140
|
+
// Path mirrors identity: the file's place in the tree is the authority for id, logical artifact,
|
|
141
|
+
// kind, plane and measure. A renamed file or a hand-edited id cannot drift apart silently.
|
|
142
|
+
const [theme, artifact] = segments, kind = match[1], plane = match[2], measure = match[3] ?? null;
|
|
143
|
+
if (text(record.kind) !== kind) findings.push(finding("planner.telemetry.tracking-plan-schema", file, `kind '${text(record.kind) || "<missing>"}' does not match file name ${name}`));
|
|
144
|
+
if (text(record.plane) !== plane) findings.push(finding("planner.telemetry.tracking-plan-schema", file, `plane '${text(record.plane) || "<missing>"}' does not match file name ${name}`));
|
|
145
|
+
if (kind === "metric") {
|
|
146
|
+
if (!measure) findings.push(finding("planner.telemetry.tracking-plan-schema", file, `metric items carry a measure segment: metric.<plane>.<measure>.json (found ${name})`));
|
|
147
|
+
else if (text(record.measure) && text(record.measure) !== measure) findings.push(finding("planner.telemetry.tracking-plan-schema", file, `measure '${text(record.measure)}' does not match file name ${name}`));
|
|
148
|
+
} else if (measure) {
|
|
149
|
+
findings.push(finding("planner.telemetry.tracking-plan-schema", file, `only metric items carry a measure segment (found ${name})`));
|
|
150
|
+
}
|
|
151
|
+
const expectedId = `telemetry:${kind}:${plane}:${theme}:${artifact}${measure ? `:${measure}` : ""}`;
|
|
152
|
+
if (text(record.id) !== expectedId) findings.push(finding("planner.telemetry.tracking-plan-schema", file, `id '${text(record.id) || "<missing>"}' does not mirror its path; expected '${expectedId}'`));
|
|
153
|
+
const expectedLogical = `telemetry:${theme}:${artifact}`;
|
|
154
|
+
if (text(record.logical_artifact) !== expectedLogical) findings.push(finding("planner.telemetry.tracking-plan-schema", file, `logical_artifact '${text(record.logical_artifact) || "<missing>"}' does not mirror its path; expected '${expectedLogical}'`));
|
|
155
|
+
const declared = record.properties && typeof record.properties === "object" && !Array.isArray(record.properties) ? Object.keys(record.properties as Record<string, unknown>) : [];
|
|
156
|
+
for (const name of (Array.isArray(record.required) ? record.required : []).map(text)) {
|
|
157
|
+
if (name && !declared.includes(name)) findings.push(finding("planner.telemetry.tracking-plan-schema", file, `required property '${name}' is not declared in properties`));
|
|
158
|
+
}
|
|
159
|
+
const id = text(record.id), previous = byId.get(id);
|
|
160
|
+
if (id && previous) findings.push(finding("planner.telemetry.tracking-plan-schema", file, `concrete telemetry '${id}' is declared by both ${previous.file} and ${file}`));
|
|
161
|
+
else if (id) byId.set(id, { file, data: record });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const plan = await loadPlan(absolute);
|
|
165
|
+
const acceptances = plan.artifacts.filter(artifact => artifact.kind === "acceptance");
|
|
166
|
+
|
|
167
|
+
// Every concrete item resolves to exactly one wagon-owned logical artifact, and owner names that wagon.
|
|
168
|
+
const owners = telemetryOwners(plan.artifacts.filter(artifact => artifact.kind === "wagon"));
|
|
169
|
+
for (const item of items) {
|
|
170
|
+
const logical = text(item.data.logical_artifact);
|
|
171
|
+
if (!logical) continue; // the schema finding already names it
|
|
172
|
+
const owning = owners.get(logical) ?? [];
|
|
173
|
+
if (!owning.length) findings.push(finding("planner.telemetry.logical-ownership", item.file, `logical artifact ${logical} is produced by no wagon; declare it in exactly one wagon's produce[] telemetry`));
|
|
174
|
+
else if (owning.length > 1) findings.push(finding("planner.telemetry.logical-ownership", item.file, `logical artifact ${logical} is produced by wagons ${owning.join(", ")}`));
|
|
175
|
+
else if (text(item.data.owner) && text(item.data.owner) !== owning[0]) findings.push(finding("planner.telemetry.logical-ownership", item.file, `owner '${text(item.data.owner)}' does not match the owning wagon '${owning[0]}'`));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Every acceptance makes an explicit decision; required items resolve into the tracking plan, and the
|
|
179
|
+
// plan's references back out of the tree resolve to declared acceptances.
|
|
180
|
+
const registryIds = new Set(items.map(item => text(item.data.id)).filter(Boolean));
|
|
181
|
+
const acceptanceIds = new Set(acceptances.map(acceptance => acceptance.id));
|
|
182
|
+
for (const acceptance of acceptances) {
|
|
183
|
+
const block = acceptance.data.telemetry;
|
|
184
|
+
if (!block || typeof block !== "object" || Array.isArray(block)) {
|
|
185
|
+
findings.push(finding("planner.telemetry.acceptance-decision", acceptance.file, `${acceptance.id} declares no telemetry decision; declare telemetry: { disposition: required, events: [...] } or telemetry: { disposition: not-applicable, rationale: ... }`));
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const { disposition, urns } = telemetryDecisionOf(acceptance);
|
|
189
|
+
if (disposition === "required") {
|
|
190
|
+
if (!urns.length) findings.push(finding("planner.telemetry.acceptance-decision", acceptance.file, `${acceptance.id} is disposition: required but declares no telemetry items`));
|
|
191
|
+
for (const urn of urns) {
|
|
192
|
+
if (!urn) { findings.push(finding("planner.telemetry.acceptance-decision", acceptance.file, `${acceptance.id} declares an empty telemetry item`)); continue; }
|
|
193
|
+
if (!CONCRETE_URN.test(urn)) findings.push(finding("planner.telemetry.acceptance-decision", acceptance.file, `${urn} is not a concrete telemetry URN telemetry:{kind}:{plane}:{theme}:{artifact}[:{measure}]`));
|
|
194
|
+
else if (!registryIds.has(urn)) findings.push(finding("planner.telemetry.acceptance-decision", acceptance.file, `${urn} has no tracking-plan entry under ${topology.telemetryRoot}/`));
|
|
195
|
+
}
|
|
196
|
+
} else if (disposition === "not-applicable") {
|
|
197
|
+
const rationale = text((block as Record<string, unknown>).rationale).trim();
|
|
198
|
+
if (rationale.length < 20) findings.push(finding("planner.telemetry.acceptance-decision", acceptance.file, `${acceptance.id} is disposition: not-applicable without a rationale (at least 20 characters)`));
|
|
199
|
+
} else {
|
|
200
|
+
findings.push(finding("planner.telemetry.acceptance-decision", acceptance.file, `${acceptance.id} telemetry.disposition must be required or not-applicable, found '${disposition || "<missing>"}'`));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const item of items) {
|
|
204
|
+
for (const reference of (Array.isArray(item.data.acceptances) ? item.data.acceptances : []).map(text)) {
|
|
205
|
+
if (reference && !acceptanceIds.has(reference)) findings.push(finding("planner.telemetry.acceptance-decision", item.file, `${text(item.data.id) || item.file} references undeclared acceptance ${reference}`));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// High-cardinality identifiers are forbidden as metric labels; they belong in event, log and trace
|
|
210
|
+
// properties where the plan justifies them.
|
|
211
|
+
for (const item of items) {
|
|
212
|
+
if (text(item.data.kind) !== "metric") continue;
|
|
213
|
+
const properties = item.data.properties && typeof item.data.properties === "object" && !Array.isArray(item.data.properties) ? item.data.properties as Record<string, Record<string, unknown>> : {};
|
|
214
|
+
for (const dimension of records(item.data.dimensions)) {
|
|
215
|
+
const name = text(dimension.name);
|
|
216
|
+
if (!name) continue;
|
|
217
|
+
if (text(dimension.cardinality) === "high") findings.push(finding("planner.telemetry.metric-cardinality", item.file, `${text(item.data.id)} dimension '${name}' is high-cardinality; carry the identifier in an event, log or trace property instead`));
|
|
218
|
+
else if (properties[name] && text(properties[name].cardinality) === "high") findings.push(finding("planner.telemetry.metric-cardinality", item.file, `${text(item.data.id)} dimension '${name}' names a property declared cardinality: high`));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return findings;
|
|
223
|
+
}
|
package/src/topology.ts
CHANGED
|
@@ -2,13 +2,14 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
|
|
5
|
-
export type Topology = { planRoot: string; sourceRoot: string; testRoot: string; e2eRoot: string };
|
|
5
|
+
export type Topology = { planRoot: string; sourceRoot: string; testRoot: string; e2eRoot: string; telemetryRoot: string };
|
|
6
6
|
|
|
7
7
|
export const defaultTopology: Topology = {
|
|
8
8
|
planRoot: "plan",
|
|
9
9
|
sourceRoot: "src/wagons",
|
|
10
10
|
testRoot: "tests/wagons",
|
|
11
11
|
e2eRoot: "e2e",
|
|
12
|
+
telemetryRoot: "telemetry",
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
const configuredKeys: Array<[keyof Topology, string]> = [
|
|
@@ -16,6 +17,7 @@ const configuredKeys: Array<[keyof Topology, string]> = [
|
|
|
16
17
|
["sourceRoot", "source_root"],
|
|
17
18
|
["testRoot", "test_root"],
|
|
18
19
|
["e2eRoot", "e2e_root"],
|
|
20
|
+
["telemetryRoot", "telemetry_root"],
|
|
19
21
|
];
|
|
20
22
|
|
|
21
23
|
function safeRelative(value: unknown): string | null {
|