@afokapu/atdd-bun 0.6.1 → 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 +89 -484
- 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/atdd_traceability_closure/detect.mjs +31 -16
- 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 +96 -13
- 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/agent.ts +15 -8
- package/src/enforce.ts +28 -3
- package/src/hooks.ts +2 -2
- package/src/index.ts +2 -0
- package/src/integrity.ts +13 -5
- package/src/telemetry-plan.ts +223 -0
- package/src/topology.ts +3 -1
- package/templates/agents/AGENTS.block.md +5 -1
package/package.json
CHANGED
|
@@ -323,6 +323,57 @@
|
|
|
323
323
|
}
|
|
324
324
|
},
|
|
325
325
|
|
|
326
|
+
"telemetry": {
|
|
327
|
+
"type": "object",
|
|
328
|
+
"description": "The acceptance's explicit telemetry decision. `required` names the concrete telemetry items whose emission this acceptance must observe; `not-applicable` records why nothing beyond the tested return value is externally useful. This block supersedes the DESCRIPTIVE half of `signal` (metrics/traces/logs/events); it does not touch the EXECUTABLE bar `signal.metric` + `signal.threshold`, which is a gate, not telemetry.",
|
|
329
|
+
"$comment": "Introduced with the telemetry tracking-plan profile. Every acceptance in an adopting repository carries exactly one of the two dispositions — the false choice between 'test every debug log' and 'have no observability requirement' is what this block removes. Items listed here must resolve to tracking-plan files under telemetry/ (planner.telemetry.acceptance-decision).",
|
|
330
|
+
"required": ["disposition"],
|
|
331
|
+
"additionalProperties": false,
|
|
332
|
+
"properties": {
|
|
333
|
+
"disposition": {
|
|
334
|
+
"type": "string",
|
|
335
|
+
"enum": ["required", "not-applicable"]
|
|
336
|
+
},
|
|
337
|
+
"rationale": {
|
|
338
|
+
"type": "string",
|
|
339
|
+
"minLength": 20,
|
|
340
|
+
"description": "Required with disposition: not-applicable — why no observable outcome beyond the tested return value is externally useful."
|
|
341
|
+
},
|
|
342
|
+
"events": {
|
|
343
|
+
"type": "array",
|
|
344
|
+
"minItems": 1,
|
|
345
|
+
"items": {"$ref": "#/definitions/telemetry_event_urn"}
|
|
346
|
+
},
|
|
347
|
+
"metrics": {
|
|
348
|
+
"type": "array",
|
|
349
|
+
"minItems": 1,
|
|
350
|
+
"items": {"$ref": "#/definitions/telemetry_metric_urn"}
|
|
351
|
+
},
|
|
352
|
+
"traces": {
|
|
353
|
+
"type": "array",
|
|
354
|
+
"minItems": 1,
|
|
355
|
+
"items": {"$ref": "#/definitions/telemetry_trace_urn"}
|
|
356
|
+
},
|
|
357
|
+
"logs": {
|
|
358
|
+
"type": "array",
|
|
359
|
+
"minItems": 1,
|
|
360
|
+
"items": {"$ref": "#/definitions/telemetry_log_urn"}
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
"allOf": [
|
|
364
|
+
{
|
|
365
|
+
"$comment": "not-applicable forbids item lists: an acceptance cannot simultaneously require nothing and require something.",
|
|
366
|
+
"if": {"properties": {"disposition": {"const": "not-applicable"}}, "required": ["disposition"]},
|
|
367
|
+
"then": {"not": {"anyOf": [{"required": ["events"]}, {"required": ["metrics"]}, {"required": ["traces"]}, {"required": ["logs"]}]}}
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"$comment": "required declares at least one concrete item; resolution against the tracking plan is a cross-artifact validator concern.",
|
|
371
|
+
"if": {"properties": {"disposition": {"const": "required"}}, "required": ["disposition"]},
|
|
372
|
+
"then": {"anyOf": [{"required": ["events"]}, {"required": ["metrics"]}, {"required": ["traces"]}, {"required": ["logs"]}]}
|
|
373
|
+
}
|
|
374
|
+
]
|
|
375
|
+
},
|
|
376
|
+
|
|
326
377
|
"metadata": {
|
|
327
378
|
"type": "object",
|
|
328
379
|
"required": ["wagon"],
|
|
@@ -389,8 +440,26 @@
|
|
|
389
440
|
"author": {"type": "string"},
|
|
390
441
|
"created": {"type": "string"}
|
|
391
442
|
}
|
|
392
|
-
}
|
|
443
|
+
},
|
|
444
|
+
"telemetry": {"$ref": "#/properties/telemetry"}
|
|
393
445
|
}
|
|
446
|
+
},
|
|
447
|
+
"telemetry_event_urn": {
|
|
448
|
+
"$comment": "Concrete telemetry URNs accepted in an acceptance's telemetry decision. Same grammar as telemetry-plan.schema.json's id, where path and ownership coherence are additionally checked.",
|
|
449
|
+
"type": "string",
|
|
450
|
+
"pattern": "^telemetry:event:(ui|ux|be|nw|db|st|tm|sc|au|fn|if):[a-z][a-z0-9-]*:[a-z][a-z0-9-]*$"
|
|
451
|
+
},
|
|
452
|
+
"telemetry_trace_urn": {
|
|
453
|
+
"type": "string",
|
|
454
|
+
"pattern": "^telemetry:trace:(ui|ux|be|nw|db|st|tm|sc|au|fn|if):[a-z][a-z0-9-]*:[a-z][a-z0-9-]*$"
|
|
455
|
+
},
|
|
456
|
+
"telemetry_log_urn": {
|
|
457
|
+
"type": "string",
|
|
458
|
+
"pattern": "^telemetry:log:(ui|ux|be|nw|db|st|tm|sc|au|fn|if):[a-z][a-z0-9-]*:[a-z][a-z0-9-]*$"
|
|
459
|
+
},
|
|
460
|
+
"telemetry_metric_urn": {
|
|
461
|
+
"type": "string",
|
|
462
|
+
"pattern": "^telemetry:metric:(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)$"
|
|
394
463
|
}
|
|
395
464
|
}
|
|
396
465
|
}
|
|
@@ -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/agent.ts
CHANGED
|
@@ -7,10 +7,14 @@ const version = (await Bun.file(join(root, "package.json")).json() as { version:
|
|
|
7
7
|
const render = async (path: string) => (await readFile(join(root, "templates/agents", path), "utf8")).replace("{{VERSION}}", version);
|
|
8
8
|
// .agents/skills is the vendor-neutral Agent Skills path (Codex, Copilot, Cursor, Gemini CLI, …); .claude/skills is Claude Code's.
|
|
9
9
|
const skillPaths = [".agents/skills/atdd/SKILL.md", ".claude/skills/atdd/SKILL.md"];
|
|
10
|
+
// AGENTS.md is read by Codex, Cursor and most agents; CLAUDE.md by Claude Code. Both carry the same block.
|
|
11
|
+
export const instructionPaths = ["AGENTS.md", "CLAUDE.md"];
|
|
10
12
|
const block = /<!-- atdd-bun:start[\s\S]*?<!-- atdd-bun:end -->\n?/;
|
|
13
|
+
/** `current` with the managed block replaced, or appended after at least one blank line; `current` itself is kept. */
|
|
14
|
+
const withBlock = (current: string, managed: string) => block.test(current) ? current.replace(block, managed) : `${current}${current ? "\n\n".slice(current.match(/\n{0,2}$/)![0].length) : ""}${managed}`;
|
|
11
15
|
|
|
12
|
-
/** Write the ATDD skill for every agent and a managed pointer block in AGENTS.md. Existing files
|
|
13
|
-
* existing block are kept unless `replace`; the rest of
|
|
16
|
+
/** Write the ATDD skill for every agent and a managed pointer block in AGENTS.md and CLAUDE.md. Existing files
|
|
17
|
+
* and an existing block are kept unless `replace`; the rest of each instruction file is never touched. */
|
|
14
18
|
export async function agentInit(repo = process.cwd(), replace = false) {
|
|
15
19
|
const written: string[] = [], kept: string[] = [];
|
|
16
20
|
for (const path of skillPaths) {
|
|
@@ -18,14 +22,17 @@ export async function agentInit(repo = process.cwd(), replace = false) {
|
|
|
18
22
|
if (existsSync(output) && !replace) { kept.push(output); continue; }
|
|
19
23
|
await mkdir(dirname(output), { recursive: true }); await writeFile(output, await render("atdd/SKILL.md")); written.push(output);
|
|
20
24
|
}
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
const managed = await render("AGENTS.block.md");
|
|
26
|
+
for (const path of instructionPaths) {
|
|
27
|
+
const file = join(repo, path), current = existsSync(file) ? await readFile(file, "utf8") : "";
|
|
28
|
+
if (block.test(current) && !replace) { kept.push(file); continue; }
|
|
29
|
+
await writeFile(file, withBlock(current, managed)); written.push(file);
|
|
30
|
+
}
|
|
24
31
|
if (!written.length) return { ok: false, message: `${kept.join(", ")} exist; use --replace` };
|
|
25
32
|
return { ok: true, message: written.join("\n") };
|
|
26
33
|
}
|
|
27
34
|
export async function agentStatus(repo = process.cwd()) {
|
|
28
|
-
const
|
|
29
|
-
if (!existsSync(agents) || !block.test(await readFile(agents, "utf8"))) missing.push(`${agents} (atdd-bun block)`);
|
|
30
|
-
return { ok: !missing.length, message: missing.length ? `missing: ${missing.join(", ")}` : [...skillPaths,
|
|
35
|
+
const missing = skillPaths.map(path => join(repo, path)).filter(path => !existsSync(path));
|
|
36
|
+
for (const path of instructionPaths) { const agents = join(repo, path); if (!existsSync(agents) || !block.test(await readFile(agents, "utf8"))) missing.push(`${agents} (atdd-bun block)`); }
|
|
37
|
+
return { ok: !missing.length, message: missing.length ? `missing: ${missing.join(", ")}` : [...skillPaths, ...instructionPaths].map(path => join(repo, path)).join("\n") };
|
|
31
38
|
}
|
package/src/enforce.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
3
|
import { tmpdir } from "node:os";
|
|
3
4
|
import { join, resolve } from "node:path";
|
|
4
5
|
import { topologyFor } from "./topology";
|
|
5
6
|
|
|
6
|
-
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";
|
|
7
8
|
|
|
8
9
|
export type Violation = {
|
|
9
10
|
rule_id: string;
|
|
@@ -26,6 +27,7 @@ const profiles: Record<Exclude<Profile, "all">, string[]> = {
|
|
|
26
27
|
topology: ["atdd_topology"],
|
|
27
28
|
docs: ["planner_docs_capability"],
|
|
28
29
|
planner: ["planner_plan_integrity", "planner_schema_validation", "planner_static_validators", "atdd_topology"],
|
|
30
|
+
telemetry: ["planner_telemetry_plan", "bun_telemetry_code", "bun_telemetry_test"],
|
|
29
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"],
|
|
30
32
|
tester: ["bun_tester_discipline_detector", "htmx_e2e_detector", "atdd_topology"],
|
|
31
33
|
security: ["bun_security_hygiene_detector"],
|
|
@@ -37,8 +39,27 @@ const profiles: Record<Exclude<Profile, "all">, string[]> = {
|
|
|
37
39
|
design: ["bun_design_system_detector", "bun_responsive_detector"],
|
|
38
40
|
};
|
|
39
41
|
|
|
42
|
+
type ConcreteProfile = Exclude<Profile, "all">;
|
|
43
|
+
/** Every profile but `all`. */
|
|
44
|
+
export const concreteProfiles = Object.keys(profiles) as ConcreteProfile[];
|
|
40
45
|
/** Profile names accepted by the CLI and public integrations. */
|
|
41
|
-
export const profileNames = [...
|
|
46
|
+
export const profileNames = [...concreteProfiles, "all"] as Profile[];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The profiles the operator has activated: `profiles:` in atdd-bun.yaml, or every profile when absent. A legacy
|
|
50
|
+
* repository can adopt enforcement gradually by listing only what it is ready for. `all`, the hooks and the
|
|
51
|
+
* generated CI run exactly these; naming a profile explicitly still runs it. An unknown name or an empty list is
|
|
52
|
+
* a configuration error, never a silent "run nothing".
|
|
53
|
+
*/
|
|
54
|
+
export async function enabledProfiles(root = process.cwd()): Promise<ConcreteProfile[]> {
|
|
55
|
+
const file = join(resolve(root), "atdd-bun.yaml");
|
|
56
|
+
const listed = existsSync(file) ? (Bun.YAML.parse(await readFile(file, "utf8")) as { profiles?: unknown } | null)?.profiles : undefined;
|
|
57
|
+
if (listed === undefined) return concreteProfiles;
|
|
58
|
+
if (!Array.isArray(listed) || !listed.length) throw new Error("atdd-bun.yaml profiles must be a non-empty list of profile names");
|
|
59
|
+
const unknown = listed.filter(name => !concreteProfiles.includes(name));
|
|
60
|
+
if (unknown.length) throw new Error(`atdd-bun.yaml profiles lists unknown profile(s): ${unknown.join(", ")}; known: ${concreteProfiles.join(", ")}`);
|
|
61
|
+
return [...new Set(listed as ConcreteProfile[])];
|
|
62
|
+
}
|
|
42
63
|
|
|
43
64
|
const packageRoot = resolve(import.meta.dir, "..");
|
|
44
65
|
const detectorRoot = join(packageRoot, "detectors");
|
|
@@ -120,8 +141,12 @@ export async function enforce(config: EnforcementConfig = {}): Promise<Violation
|
|
|
120
141
|
const root = resolve(config.root ?? process.cwd());
|
|
121
142
|
const scanRoots = (config.scanRoots?.length ? config.scanRoots : [root]).map((path) => resolve(root, path));
|
|
122
143
|
const excludes = ["node_modules", ".git", ".atdd", ...(config.excludes ?? [])];
|
|
144
|
+
// `all` means every profile the operator activated; explicitly named profiles run as asked. The operator's list
|
|
145
|
+
// is validated on every run, so a misspelled atdd-bun.yaml fails whichever profile is requested.
|
|
146
|
+
const requested = config.profiles ?? ["all"], enabled = await enabledProfiles(root);
|
|
147
|
+
const selected = requested.includes("all") ? [...new Set([...requested.filter(p => p !== "all"), ...enabled])] : requested;
|
|
123
148
|
const results = await Promise.all(
|
|
124
|
-
implementationsFor(
|
|
149
|
+
implementationsFor(selected).map((implementation) => runImplementation(implementation, { scanRoots, excludes })),
|
|
125
150
|
);
|
|
126
151
|
return results.flat().sort((left, right) =>
|
|
127
152
|
left.rule_id.localeCompare(right.rule_id) || left.file.localeCompare(right.file) || left.line - right.line,
|
package/src/hooks.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { chmod, mkdir, readdir, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
-
import { enforce } from "./enforce";
|
|
4
|
+
import { enabledProfiles, enforce } from "./enforce";
|
|
5
5
|
import { topologyFor } from "./topology";
|
|
6
6
|
|
|
7
7
|
export type WorktreePolicy = { enabled: boolean; root: string; primary_directory: string; primary_branch: string; require_linked_worktree: boolean };
|
|
@@ -19,7 +19,7 @@ const isRegistry = (cfg: HookPolicy, path: string) => cfg.registry_paths.some(pa
|
|
|
19
19
|
const stagedStats = async (root: string) => (await git(root, ["diff", "--cached", "--numstat", "--no-renames"])).out.split("\n").filter(Boolean).map(row => { const [added, removed, ...path] = row.split("\t"); return { added: Number(added) || 0, removed: Number(removed) || 0, path: path.join("\t") }; });
|
|
20
20
|
|
|
21
21
|
export async function policy(root: string): Promise<HookPolicy> { const file = join(root, "atdd-bun.yaml"); if (!existsSync(file)) return defaultHookPolicy; const data = Bun.YAML.parse(await readFile(file, "utf8")) as Record<string, unknown>; const configuredWorktrees = data?.worktrees && typeof data.worktrees === "object" && !Array.isArray(data.worktrees) ? data.worktrees as Record<string, unknown> : {}; return { ...defaultHookPolicy, ...Object.fromEntries(Object.entries(data ?? {}).filter(([key, value]) => key in defaultHookPolicy && key !== "worktrees" && typeof value === typeof (defaultHookPolicy as any)[key])), protected_branches: strings(data?.protected_branches) ?? defaultHookPolicy.protected_branches, registry_paths: strings(data?.registry_paths) ?? defaultHookPolicy.registry_paths, worktrees: { ...defaultWorktreePolicy, ...Object.fromEntries(Object.entries(configuredWorktrees).filter(([key, value]) => key in defaultWorktreePolicy && typeof value === typeof (defaultWorktreePolicy as any)[key])) } }; }
|
|
22
|
-
async function validation(root: string, changed: string[], full: boolean, registryChanged = false) { const profiles: any[] = [], topology = await topologyFor(root); if (registryChanged || changed.some(path => path.startsWith(`${topology.planRoot}/`)) || changed.includes("atdd-bun.yaml")) profiles.push("planner", "traceability"); if (changed.some(path => /\.(?:[cm]?[jt]sx?|html)$/.test(path))) profiles.push("coder", "tester"); if (changed.includes("atdd-bun.yaml") || changed.some(path => /\.ya?ml$/.test(path))) profiles.push("topology"); if (!profiles.length) return { ok: true, message: "no affected area" }; try { const
|
|
22
|
+
async function validation(root: string, changed: string[], full: boolean, registryChanged = false) { const profiles: any[] = [], topology = await topologyFor(root); if (registryChanged || changed.some(path => path.startsWith(`${topology.planRoot}/`)) || changed.includes("atdd-bun.yaml")) profiles.push("planner", "traceability"); if (changed.some(path => /\.(?:[cm]?[jt]sx?|html)$/.test(path))) profiles.push("coder", "tester"); if (changed.includes("atdd-bun.yaml") || changed.some(path => /\.ya?ml$/.test(path))) profiles.push("topology"); if (!profiles.length) return { ok: true, message: "no affected area" }; try { const enabled = await enabledProfiles(root), selected = full ? ["all" as const] : [...new Set(profiles)].filter(profile => enabled.includes(profile)); if (!selected.length) return { ok: true, message: "no activated profile covers the affected area" }; const findings = await enforce({ root, profiles: selected }); return findings.length ? bad(findings.map(f => `${f.rule_id}: ${f.file}`).join("\n")) : { ok: true, message: "validation clean" }; } catch (error) { return bad(`Bun enforcer resolution failed: ${String(error)}`); } }
|
|
23
23
|
|
|
24
24
|
const inside = (parent: string, child: string) => { const path = relative(parent, child); return path === "" || (path !== ".." && !path.startsWith("../") && !path.startsWith("..\\")); };
|
|
25
25
|
async function primaryRoot(root: string): Promise<string | null> { const result = await git(root, ["rev-parse", "--git-common-dir"]); if (result.code) return null; return dirname(resolve(root, result.out)); }
|
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";
|
package/src/integrity.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { instructionPaths } from "./agent";
|
|
5
|
+
import { concreteProfiles } from "./enforce";
|
|
4
6
|
import { defaultHookPolicy, type HookPolicy } from "./hooks";
|
|
5
7
|
|
|
6
8
|
/**
|
|
@@ -95,15 +97,17 @@ async function checkGenerated(root: string, packageRoot: string): Promise<Integr
|
|
|
95
97
|
await same(WORKFLOW, "templates/github/atdd-bun.yml", "bun run atdd-bun ci init --replace");
|
|
96
98
|
for (const skill of SKILLS) await same(skill, "templates/agents/atdd/SKILL.md", "bun run atdd-bun agent init --replace");
|
|
97
99
|
await same(relative(root, await testFilePath(root)), "templates/agents/atdd-bun.integrity.test.ts", "bun run atdd-bun integrity init --replace");
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
const canonical = (await readFile(join(packageRoot, "templates/agents/AGENTS.block.md"), "utf8")).match(BLOCK)![0];
|
|
101
|
+
for (const file of instructionPaths) {
|
|
102
|
+
const agents = existsSync(join(root, file)) ? await readFile(join(root, file), "utf8") : "", block = agents.match(BLOCK)?.[0];
|
|
103
|
+
if (!block) findings.push({ file, detail: "is missing the atdd-bun block", restore: "bun run atdd-bun agent init --replace" });
|
|
104
|
+
else if (unstamp(block) !== unstamp(canonical)) findings.push({ file, detail: "atdd-bun block was edited", restore: "bun run atdd-bun agent init --replace" });
|
|
105
|
+
}
|
|
102
106
|
return findings;
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
/** Names of the policy fields in `current` that are looser than in `base`. */
|
|
106
|
-
export function loosenedPolicy(base: Partial<HookPolicy
|
|
110
|
+
export function loosenedPolicy(base: Partial<HookPolicy> & { profiles?: unknown }, current: Partial<HookPolicy> & { profiles?: unknown }): string[] {
|
|
107
111
|
const b = { ...defaultHookPolicy, ...base, worktrees: { ...defaultHookPolicy.worktrees, ...base.worktrees } }, c = { ...defaultHookPolicy, ...current, worktrees: { ...defaultHookPolicy.worktrees, ...current.worktrees } };
|
|
108
112
|
const out: string[] = [];
|
|
109
113
|
for (const key of ["max_staged_files", "max_staged_changed_lines", "max_uncommitted_files", "max_commits_per_push", "max_registry_removed_lines"] as const) if (Number(c[key]) > Number(b[key])) out.push(`${key} ${b[key]} → ${c[key]}`);
|
|
@@ -112,6 +116,10 @@ export function loosenedPolicy(base: Partial<HookPolicy>, current: Partial<HookP
|
|
|
112
116
|
const removed = b.protected_branches.filter(x => !c.protected_branches.includes(x)), added = c.registry_paths.filter(x => !b.registry_paths.includes(x));
|
|
113
117
|
if (removed.length) out.push(`protected_branches drops ${removed.join(", ")}`);
|
|
114
118
|
if (added.length) out.push(`registry_paths adds ${added.join(", ")}`);
|
|
119
|
+
// Deactivating a profile stops enforcing it; the operator may do it, as a change a human approves.
|
|
120
|
+
const active = (config: { profiles?: unknown }): string[] => Array.isArray(config.profiles) ? config.profiles.map(String) : concreteProfiles;
|
|
121
|
+
const dropped = active(base).filter(name => !active(current).includes(name));
|
|
122
|
+
if (dropped.length) out.push(`profiles drops ${dropped.join(", ")}`);
|
|
115
123
|
return out;
|
|
116
124
|
}
|
|
117
125
|
|