Skip to content

Commit adf58f4

Browse files
committed
feat(checklist): hash transitive dependencies in requirement/architecture checklists
requirements_checklist and architecture_checklist now follow the sphinx-needs link graph recursively from the elements in deps and hash the whole closure, so a checklist goes out of date when an upstream dependency (e.g. a linked stakeholder requirement) changes, not only when a root element changes. - validate_checklist.py: add transitive mode via repeatable --link-field; follow links recursively, normalize version-constrained link targets (id[version==1] -> id), hash canonical serialization of the closure. - docs.bzl: add link_fields arg to both macros (defaults cover the requirement / architecture link chains); pass --link-field through. link_fields = [] restores the previous flat hashing. - metamodel.yaml + README_needs_rules.md: document the recursive behavior.
1 parent 7c10704 commit adf58f4

6 files changed

Lines changed: 557 additions & 225 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# How-to: S-Core process step-by-step
2+
3+
## 1. Feature & Feature requirements in main score repository
4+
5+
### a. What to do
6+
7+
1. Define Feature
8+
2. Define Feature Requirements and map them to the stakeholder requirements
9+
10+
### b. How to check
11+
12+
1. Automated checks for requirements in docs-as-code.
13+
2. Checklist for things, that can not be automated -> automated check, that checklist is up to date.

docs.bzl

Lines changed: 112 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -102,31 +102,30 @@ def filtered_needs_json(
102102
name,
103103
src,
104104
types = [],
105-
components = [],
106-
component_attr = "component",
105+
names = [],
107106
visibility = None):
108107
"""Extract a subset of sphinx-needs elements from a needs.json file.
109108
110109
Produces a `<name>.json` file containing only the needs that match all of
111110
the given filters. This is useful to hand a downstream consumer just the
112-
elements (e.g. `feat_req`) of one or more particular components.
111+
elements (e.g. `feat_req`) of one or more particular features/components.
113112
114113
Args:
115114
name: Name of the generated target. The output file is `<name>.json`.
116115
src: Label of a `needs_json` build output (a directory containing
117116
`needs.json`), e.g. `":needs_json"` or `"@score_process//:needs_json"`.
118117
types: Optional list of sphinx-needs element types to keep
119118
(e.g. `["feat_req", "comp_req"]`). If empty, all types are kept.
120-
components: Optional list of component names to keep. If empty, all
121-
components are kept.
122-
component_attr: Need attribute matched against `components`.
123-
Defaults to `"component"`.
119+
names: Optional list of feature/component names to keep, matched against
120+
the second `__`-separated segment of each need ID (the
121+
`<type>__<name>__...` naming convention). If empty, all
122+
features/components are kept.
124123
visibility: Standard Bazel visibility for the generated target.
125124
"""
126125
filter_tool = Label("//scripts_bazel:filter_needs_json")
127126

128127
type_args = " ".join(["--type '%s'" % t for t in types])
129-
component_args = " ".join(["--component '%s'" % c for c in components])
128+
name_args = " ".join(["--name '%s'" % n for n in names])
130129

131130
native.genrule(
132131
name = name,
@@ -135,15 +134,13 @@ def filtered_needs_json(
135134
cmd = """
136135
$(location {filter_tool}) \
137136
--output $@ \
138-
--component-attr '{component_attr}' \
139137
{type_args} \
140-
{component_args} \
138+
{name_args} \
141139
$(location {src})/needs.json
142140
""".format(
143141
filter_tool = filter_tool,
144-
component_attr = component_attr,
145142
type_args = type_args,
146-
component_args = component_args,
143+
name_args = name_args,
147144
src = src,
148145
),
149146
tools = [filter_tool],
@@ -165,16 +162,16 @@ def component_requirements(
165162
src: Label of a `needs_json` build output. Defaults to the calling
166163
package's `//:needs_json`.
167164
component: Optional component name. If given, only component requirements
168-
tagged with that component are kept; if omitted, all component
169-
requirements are kept.
165+
named with that component (per the `<type>__<name>__...`
166+
convention) are kept; if omitted, all component requirements are
167+
kept.
170168
visibility: Standard Bazel visibility for the generated target.
171169
"""
172170
filtered_needs_json(
173171
name = name,
174172
src = src,
175173
types = ["comp_req"],
176-
components = [component] if component else [],
177-
component_attr = "tags",
174+
names = [component] if component else [],
178175
visibility = visibility,
179176
)
180177

@@ -193,16 +190,15 @@ def feature_requirements(
193190
src: Label of a `needs_json` build output. Defaults to the calling
194191
package's `//:needs_json`.
195192
feature: Optional feature name. If given, only feature requirements
196-
tagged with that feature are kept; if omitted, all feature
197-
requirements are kept.
193+
named with that feature (per the `<type>__<name>__...` convention)
194+
are kept; if omitted, all feature requirements are kept.
198195
visibility: Standard Bazel visibility for the generated target.
199196
"""
200197
filtered_needs_json(
201198
name = name,
202199
src = src,
203200
types = ["feat_req"],
204-
components = [feature] if feature else [],
205-
component_attr = "tags",
201+
names = [feature] if feature else [],
206202
visibility = visibility,
207203
)
208204

@@ -221,16 +217,15 @@ def assumptions_of_use(
221217
src: Label of a `needs_json` build output. Defaults to the calling
222218
package's `//:needs_json`.
223219
component: Optional component name. If given, only assumptions of use
224-
tagged with that component are kept; if omitted, all assumptions of
225-
use are kept.
220+
named with that component (per the `<type>__<name>__...`
221+
convention) are kept; if omitted, all assumptions of use are kept.
226222
visibility: Standard Bazel visibility for the generated target.
227223
"""
228224
filtered_needs_json(
229225
name = name,
230226
src = src,
231227
types = ["aou_req"],
232-
components = [component] if component else [],
233-
component_attr = "tags",
228+
names = [component] if component else [],
234229
visibility = visibility,
235230
)
236231

@@ -251,16 +246,16 @@ def feature_architecture(
251246
src: Label of a `needs_json` build output. Defaults to the calling
252247
package's `//:needs_json`.
253248
feature: Optional feature name. If given, only feature architecture
254-
elements tagged with that feature are kept; if omitted, all feature
255-
architecture elements are kept.
249+
elements named with that feature (per the `<type>__<name>__...`
250+
convention) are kept; if omitted, all feature architecture elements
251+
are kept.
256252
visibility: Standard Bazel visibility for the generated target.
257253
"""
258254
filtered_needs_json(
259255
name = name,
260256
src = src,
261257
types = ["feat_arc_sta", "feat_arc_dyn"],
262-
components = [feature] if feature else [],
263-
component_attr = "tags",
258+
names = [feature] if feature else [],
264259
visibility = visibility,
265260
)
266261

@@ -281,16 +276,16 @@ def component_architecture(
281276
src: Label of a `needs_json` build output. Defaults to the calling
282277
package's `//:needs_json`.
283278
component: Optional component name. If given, only component architecture
284-
elements tagged with that component are kept; if omitted, all
285-
component architecture elements are kept.
279+
elements named with that component (per the `<type>__<name>__...`
280+
convention) are kept; if omitted, all component architecture
281+
elements are kept.
286282
visibility: Standard Bazel visibility for the generated target.
287283
"""
288284
filtered_needs_json(
289285
name = name,
290286
src = src,
291287
types = ["comp_arc_sta", "comp_arc_dyn"],
292-
components = [component] if component else [],
293-
component_attr = "tags",
288+
names = [component] if component else [],
294289
visibility = visibility,
295290
)
296291

@@ -386,14 +381,26 @@ def requirements_checklist(
386381
checklist_id,
387382
deps,
388383
src = "//:needs_json",
384+
link_fields = ["derived_from", "satisfies", "covers"],
385+
extra_needs = [],
389386
visibility = None):
390387
"""Validate a requirement checklist (`req_chklst`) against its build output.
391388
392-
Building this target recomputes the SHA256 over the concatenated outputs of
393-
`deps` and compares it to the `sha256` attribute of the `req_chklst` need
394-
`checklist_id` (looked up in `src`'s `needs.json`). The build **fails** when
395-
the hashes differ, i.e. when a validated target output has changed since the
396-
checklist was last reviewed.
389+
Building this target recomputes the SHA256 over the requirements in `deps`
390+
**and**, by default, over everything they depend on transitively, and
391+
compares it to the `sha256` attribute of the `req_chklst` need `checklist_id`
392+
(looked up in `src`'s `needs.json`). The build **fails** when the hashes
393+
differ, i.e. when a validated requirement *or one of its (recursive)
394+
dependencies* has changed since the checklist was last reviewed.
395+
396+
The dependency graph is the sphinx-needs link graph: starting from the
397+
requirements in `deps` (the *roots*), the `link_fields` (by default
398+
`derived_from`, `satisfies` and `covers`) are followed recursively through
399+
`src`'s `needs.json`. For feature requirements this means the linked
400+
stakeholder requirements (and their parents in turn) are part of the hash, so
401+
changing a relevant stakeholder requirement makes this checklist go out of
402+
date. Pass `link_fields = []` to restore the old behaviour of hashing only
403+
the requirements in `deps`.
397404
398405
Typical usage validates the extracted requirements of a component against the
399406
checklist that reviewed them:
@@ -418,30 +425,50 @@ def requirements_checklist(
418425
name: Name of the generated target. The output file is `<name>.sha256`.
419426
checklist_id: Id of the `req_chklst` need to validate
420427
(e.g. `"req_chklst__bitmanipulation__comp_req"`).
421-
deps: List of labels whose outputs are hashed and validated. Usually a
422-
single `component_requirements`/`filtered_needs_json` target.
423-
src: Label of a `needs_json` build output containing the checklist need.
424-
Defaults to the calling package's `//:needs_json`.
428+
deps: List of labels whose outputs define the root requirements that are
429+
hashed and validated. Usually a single
430+
`component_requirements`/`feature_requirements`/`filtered_needs_json`
431+
target.
432+
src: Label of a `needs_json` build output containing the checklist need
433+
and the full link graph. Defaults to the calling package's
434+
`//:needs_json`.
435+
link_fields: Sphinx-needs link fields followed recursively from the root
436+
requirements to include their (transitive) dependencies in the hash.
437+
Defaults to `["derived_from", "satisfies", "covers"]`. Set to `[]` to
438+
hash only the requirements in `deps`.
439+
extra_needs: Optional list of additional `needs_json` build outputs that
440+
provide the full content of needs referenced from the validated
441+
requirements but not contained in `src` (e.g. stakeholder
442+
requirements imported from an upstream repository). Without them such
443+
external needs are hashed as `<MISSING>` and changes to them are not
444+
detected. Typically the same upstream `needs_json` targets passed as
445+
`data` to `docs(...)` (e.g. `["@score_platform//:needs_json"]`).
425446
visibility: Standard Bazel visibility for the generated target.
426447
"""
427448
validate_tool = Label("//scripts_bazel:validate_checklist")
428449

429450
dep_args = " ".join(["$(locations %s)" % d for d in deps])
451+
link_args = " ".join(["--link-field '%s'" % f for f in link_fields])
452+
extra_args = " ".join(["--extra-needs-json $(location %s)/needs.json" % e for e in extra_needs])
430453

431454
native.genrule(
432455
name = name,
433-
srcs = [src] + deps,
456+
srcs = [src] + extra_needs + deps,
434457
outs = [name + ".sha256"],
435458
cmd = """
436459
$(location {validate_tool}) \
437460
--needs-json $(location {src})/needs.json \
438461
--checklist-id '{checklist_id}' \
439462
--output $@ \
463+
{link_args} \
464+
{extra_args} \
440465
{dep_args}
441466
""".format(
442467
validate_tool = validate_tool,
443468
checklist_id = checklist_id,
444469
src = src,
470+
link_args = link_args,
471+
extra_args = extra_args,
445472
dep_args = dep_args,
446473
),
447474
tools = [validate_tool],
@@ -453,14 +480,27 @@ def architecture_checklist(
453480
checklist_id,
454481
deps,
455482
src = "//:needs_json",
483+
link_fields = ["fulfils", "includes", "uses", "provides", "derived_from", "satisfies", "covers"],
484+
extra_needs = [],
456485
visibility = None):
457486
"""Validate an architecture checklist (`arch_chklst`) against its build output.
458487
459-
Building this target recomputes the SHA256 over the concatenated outputs of
460-
`deps` and compares it to the `sha256` attribute of the `arch_chklst` need
461-
`checklist_id` (looked up in `src`'s `needs.json`). The build **fails** when
462-
the hashes differ, i.e. when a validated target output has changed since the
463-
checklist was last reviewed.
488+
Building this target recomputes the SHA256 over the architecture in `deps`
489+
**and**, by default, over everything they depend on transitively, and
490+
compares it to the `sha256` attribute of the `arch_chklst` need `checklist_id`
491+
(looked up in `src`'s `needs.json`). The build **fails** when the hashes
492+
differ, i.e. when a validated architecture element *or one of its (recursive)
493+
dependencies* has changed since the checklist was last reviewed.
494+
495+
The dependency graph is the sphinx-needs link graph: starting from the
496+
architecture elements in `deps` (the *roots*), the `link_fields` are followed
497+
recursively through `src`'s `needs.json`. The defaults follow the structural
498+
architecture links (`includes`, `uses`, `provides`) as well as `fulfils` and
499+
the requirement links (`derived_from`, `satisfies`, `covers`), so the closure
500+
reaches the fulfilled requirements and their parents. Changing a fulfilled
501+
requirement (or a stakeholder requirement it derives from) therefore makes
502+
this checklist go out of date. Pass `link_fields = []` to restore the old
503+
behaviour of hashing only the elements in `deps`.
464504
465505
Typical usage validates the extracted architecture of a component against the
466506
checklist that reviewed it:
@@ -485,31 +525,50 @@ def architecture_checklist(
485525
name: Name of the generated target. The output file is `<name>.sha256`.
486526
checklist_id: Id of the `arch_chklst` need to validate
487527
(e.g. `"arch_chklst__bitmanipulation__comp_arc"`).
488-
deps: List of labels whose outputs are hashed and validated. Usually a
489-
single `feature_architecture`/`component_architecture`/
490-
`filtered_needs_json` target.
491-
src: Label of a `needs_json` build output containing the checklist need.
492-
Defaults to the calling package's `//:needs_json`.
528+
deps: List of labels whose outputs define the root architecture elements
529+
that are hashed and validated. Usually a single
530+
`feature_architecture`/`component_architecture`/`filtered_needs_json`
531+
target.
532+
src: Label of a `needs_json` build output containing the checklist need
533+
and the full link graph. Defaults to the calling package's
534+
`//:needs_json`.
535+
link_fields: Sphinx-needs link fields followed recursively from the root
536+
architecture elements to include their (transitive) dependencies in
537+
the hash. Defaults to the structural architecture links plus the
538+
requirement links. Set to `[]` to hash only the elements in `deps`.
539+
extra_needs: Optional list of additional `needs_json` build outputs that
540+
provide the full content of needs referenced from the validated
541+
architecture elements but not contained in `src` (e.g. feature
542+
requirements imported from an upstream repository). Without them such
543+
external needs are hashed as `<MISSING>` and changes to them are not
544+
detected. Typically the same upstream `needs_json` targets passed as
545+
`data` to `docs(...)` (e.g. `["@score_platform//:needs_json"]`).
493546
visibility: Standard Bazel visibility for the generated target.
494547
"""
495548
validate_tool = Label("//scripts_bazel:validate_checklist")
496549

497550
dep_args = " ".join(["$(locations %s)" % d for d in deps])
551+
link_args = " ".join(["--link-field '%s'" % f for f in link_fields])
552+
extra_args = " ".join(["--extra-needs-json $(location %s)/needs.json" % e for e in extra_needs])
498553

499554
native.genrule(
500555
name = name,
501-
srcs = [src] + deps,
556+
srcs = [src] + extra_needs + deps,
502557
outs = [name + ".sha256"],
503558
cmd = """
504559
$(location {validate_tool}) \
505560
--needs-json $(location {src})/needs.json \
506561
--checklist-id '{checklist_id}' \
507562
--output $@ \
563+
{link_args} \
564+
{extra_args} \
508565
{dep_args}
509566
""".format(
510567
validate_tool = validate_tool,
511568
checklist_id = checklist_id,
512569
src = src,
570+
link_args = link_args,
571+
extra_args = extra_args,
513572
dep_args = dep_args,
514573
),
515574
tools = [validate_tool],

0 commit comments

Comments
 (0)