Spaces:
Sleeping
fix(constructs): correct the review pass after a validity review
Browse filesA research-validity review of the previous commit found four real
problems. All four are fixed here.
Two of the reviewer's wording corrections moved an item AWAY from the
publication its construct cites, and I had applied them under a
`verified` badge. Checked against the sources: the published MFQ30
reads "treated differently than others" (not "from"), and Edmondson
(1999) reads "It is safe to take a risk on this team" (not "in") - the
proposed "in this team" comes from a third-party questionnaire, whose
URL I had also promoted over the working Edmondson PDF. Both items keep
their published wording, both constructs are held back as unverified
with the disagreement recorded, and the source_url override is dropped.
The reviewer's item-ORDER findings for rses and cbi_work_related_burnout
were silently dropped: my classifier only read notes on rows that failed
one of the four checks, and these sat on rows marked OK. Order feeds
item_hash and decides what sim_item_N means, so both are now recorded
and held back rather than stamped verified. I re-scanned all 525 rows;
those two are the only order findings, and the 27 other constructs with
unread notes carried only confirmations.
The spec and changelog claimed all-reversed constructs have their
aggregate "blocked under exclude_reversed". exclude_reversed is not
implemented - runs use adjustment_strategy: none - so nothing was
blocked, and the results page was telling users a higher score means
more of the construct while four constructs now score the opposite way.
Adds CONSTRUCT_ALL_ITEMS_REVERSED, emitted per scored construct
(including an anchored run's opposite pole), and the results page defers
to it. The validator's own message is corrected too.
A page range was added to the Triandis & Gelfand citation that the
reviewer never supplied, under a comment claiming nothing was inferred.
Removed.
Also: three constructs with no reachable source document stay
unverified rather than claiming verbatim verification; sync_library logs
verification_status changes so a restart cannot silently revert a
maintainer; the sheet reader now rejects a blank reverse-scoring cell
instead of reading it as "not reversed"; the script deletes version
files it no longer needs and its dry run prints every item-level change;
and the append-only test now compares against the committed copy in git
rather than v1 against v2, which would have passed even if v1 had been
edited.
Net: 21 new versions (was 23), 81 verified (was 88), 13 unverified (was
6) each with a recorded reason.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- CHANGELOG.md +15 -6
- README.md +7 -5
- ROADMAP.md +18 -3
- backend/app/construct_lib.py +15 -2
- backend/app/jobs.py +20 -0
- backend/static/assets/{index-CCWGjEOt.js → index-C8cIJp9I.js} +0 -0
- backend/static/index.html +1 -1
- backend/tests/test_construct_library_versions.py +122 -9
- docs/specs/0007-construct-library-verification-pass.md +92 -33
- frontend/src/ResultsView.jsx +9 -1
- packages/construct_library/apply_review.py +107 -23
- packages/construct_library/constructs/bas_2_v2.yaml +5 -4
- packages/construct_library/constructs/cage_questionnaire_v2.yaml +5 -4
- packages/construct_library/constructs/cbi_work_related_burnout.yaml +5 -3
- packages/construct_library/constructs/collectivism_horizontal.yaml +1 -1
- packages/construct_library/constructs/individualism_horizontal_v2.yaml +1 -1
- packages/construct_library/constructs/mfq_care.yaml +3 -3
- packages/construct_library/constructs/mfq_fairness.yaml +5 -4
- packages/construct_library/constructs/mfq_fairness_v2.yaml +0 -30
- packages/construct_library/constructs/rses_v2.yaml +5 -2
- packages/construct_library/constructs/team_psychological_safety_scale.yaml +7 -5
- packages/construct_library/constructs/team_psychological_safety_scale_v2.yaml +0 -41
- packages/construct_library/validate_constructs.py +7 -1
|
@@ -7,16 +7,25 @@ Format: [Keep a Changelog](https://keepachangelog.com/). User-visible changes on
|
|
| 7 |
### Changed
|
| 8 |
- Construct library verified against the source publications. All 525 items across the
|
| 9 |
94 constructs were reviewed (wording, reverse-scoring keys, subscale grouping,
|
| 10 |
-
citations);
|
| 11 |
when, instead of the blanket "needs verification" flag every construct carried. The
|
| 12 |
review corrected 61 reverse-scoring flags across 14 constructs (the library had 35
|
| 13 |
-
flagged items, it now has 96),
|
| 14 |
and a batch of dead or incomplete citation links. Corrected constructs ship as a new
|
| 15 |
version: the picker offers the corrected one, and runs that used the earlier version
|
| 16 |
-
still open, export, and reproduce exactly as before.
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
- Model picker timing estimates are real measurements instead of placeholders. Every
|
| 21 |
model in the registry now records seconds per 1,000 texts for short, medium, and long
|
| 22 |
texts, with the machine and date they were measured on.
|
|
|
|
| 7 |
### Changed
|
| 8 |
- Construct library verified against the source publications. All 525 items across the
|
| 9 |
94 constructs were reviewed (wording, reverse-scoring keys, subscale grouping,
|
| 10 |
+
citations); 81 constructs are now marked verified and show who reviewed them and
|
| 11 |
when, instead of the blanket "needs verification" flag every construct carried. The
|
| 12 |
review corrected 61 reverse-scoring flags across 14 constructs (the library had 35
|
| 13 |
+
flagged items, it now has 96), 7 item wordings, one subscale grouping in the SCS-SF,
|
| 14 |
and a batch of dead or incomplete citation links. Corrected constructs ship as a new
|
| 15 |
version: the picker offers the corrected one, and runs that used the earlier version
|
| 16 |
+
still open, export, and reproduce exactly as before. 13 constructs stay flagged, each
|
| 17 |
+
now saying why: two open wording questions (the IPIP scales and the K10), two where
|
| 18 |
+
the review's proposed wording disagreed with the cited publication and the published
|
| 19 |
+
wording was kept, two with unresolved item-ordering questions, and three with no
|
| 20 |
+
reachable source document on file. (spec 0007)
|
| 21 |
+
|
| 22 |
+
### Added
|
| 23 |
+
- Runs now warn when every item in a construct is reverse-scored. Scores are raw
|
| 24 |
+
similarities with no reverse adjustment, so for such a construct a higher score means
|
| 25 |
+
the text expresses the OPPOSITE of the construct's name. The results page previously
|
| 26 |
+
told every run that higher meant more of the construct; it now defers to this warning,
|
| 27 |
+
which names the construct affected. Four library constructs are in this position after
|
| 28 |
+
the review: De Jong Gierveld social loneliness and the three SCS-SF negative subscales.
|
| 29 |
- Model picker timing estimates are real measurements instead of placeholders. Every
|
| 30 |
model in the registry now records seconds per 1,000 texts for short, medium, and long
|
| 31 |
texts, with the machine and date they were measured on.
|
|
@@ -53,11 +53,13 @@ Configuration: copy `.env.example` to `.env` and fill what you need.
|
|
| 53 |
- Do not upload sensitive or identifiable data to this shared dev instance; anonymous
|
| 54 |
storage is ephemeral and the instance may reset.
|
| 55 |
- The construct library ships the lab's full collection of 94 constructs. Item wording,
|
| 56 |
-
reverse-scoring keys, subscale grouping, and citations were reviewed against the
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
| 61 |
downloads needed).
|
| 62 |
|
| 63 |
## License
|
|
|
|
| 53 |
- Do not upload sensitive or identifiable data to this shared dev instance; anonymous
|
| 54 |
storage is ephemeral and the instance may reset.
|
| 55 |
- The construct library ships the lab's full collection of 94 constructs. Item wording,
|
| 56 |
+
reverse-scoring keys, subscale grouping, and citations were reviewed against the source
|
| 57 |
+
publications in August 2026: 81 constructs are marked verified and carry the reviewer
|
| 58 |
+
and review date. The other 13 stay flagged in the interface and each records why - an
|
| 59 |
+
open wording question, a proposed correction that disagreed with the cited paper, an
|
| 60 |
+
unresolved item-ordering question, or no reachable source document. See
|
| 61 |
+
`docs/specs/0007-construct-library-verification-pass.md`.
|
| 62 |
+
- Tests: `cd backend && CCR_FAKE_EMBEDDINGS=1 python -m pytest -q` (137 tests, no ML
|
| 63 |
downloads needed).
|
| 64 |
|
| 65 |
## License
|
|
@@ -52,9 +52,24 @@ order. Detailed specs for the numbered features live in the sections below.
|
|
| 52 |
removed and README now states the license and separates it from the
|
| 53 |
questionnaire items, which belong to their original authors.
|
| 54 |
- [x] Construct library verification pass: DONE 2026-08-26 (spec 0007). Noor's
|
| 55 |
-
review of all 525 items applied;
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
- [ ] Verify Dr. Chen's maintainer pre-assignment exists on /admin and that
|
| 59 |
she can sign in.
|
| 60 |
- [ ] Self-service password reset (currently admin-only; tied to the planned
|
|
|
|
| 52 |
removed and README now states the license and separates it from the
|
| 53 |
questionnaire items, which belong to their original authors.
|
| 54 |
- [x] Construct library verification pass: DONE 2026-08-26 (spec 0007). Noor's
|
| 55 |
+
review of all 525 items applied; 81 of 94 constructs verified.
|
| 56 |
+
- [ ] Construct library: 13 constructs still unverified, each with the reason
|
| 57 |
+
in its review.notes. Needs decisions, roughly in priority order:
|
| 58 |
+
* PI: the IPIP "I" prefix (50 items, 5 constructs) and restoring the K10
|
| 59 |
+
stem (10 items).
|
| 60 |
+
* Reconcile two reviewer corrections that contradict the cited paper -
|
| 61 |
+
mfq_fairness ("differently than/from others", MFQ30 says "than") and
|
| 62 |
+
team_psychological_safety_scale ("risk on/in this team", Edmondson
|
| 63 |
+
1999 says "on"). Published wording kept for now.
|
| 64 |
+
* Decide the item ORDER for rses and cbi_work_related_burnout: the
|
| 65 |
+
reviewer says both differ from the source. Order feeds item_hash and
|
| 66 |
+
the sim_item_N export columns, so this is a scoring-visible change.
|
| 67 |
+
* Find a reachable source document for bas_2, cage_questionnaire,
|
| 68 |
+
mfq_care and mfq_fairness.
|
| 69 |
+
- [ ] Durable source links: the Grit-S entries now point at personal Dropbox
|
| 70 |
+
URLs, and dirty_dozen_* traded a working ResearchGate link for a
|
| 71 |
+
paywalled PsycNET one (the reviewer's link carried a session token that
|
| 72 |
+
could not be committed). Both want a stable replacement.
|
| 73 |
- [ ] Verify Dr. Chen's maintainer pre-assignment exists on /admin and that
|
| 74 |
she can sign in.
|
| 75 |
- [ ] Self-service password reset (currently admin-only; tied to the planned
|
|
@@ -93,8 +93,21 @@ def sync_library(db: Session) -> dict:
|
|
| 93 |
f for f, read in _MUTABLE_FIELDS.items() if getattr(existing, f) != read(c)
|
| 94 |
]
|
| 95 |
for f in changed:
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
continue
|
| 99 |
|
| 100 |
db.add(
|
|
|
|
| 93 |
f for f, read in _MUTABLE_FIELDS.items() if getattr(existing, f) != read(c)
|
| 94 |
]
|
| 95 |
for f in changed:
|
| 96 |
+
new = _MUTABLE_FIELDS[f](c)
|
| 97 |
+
if f == "verification_status":
|
| 98 |
+
# A maintainer can set this from /admin, and YAML wins on the
|
| 99 |
+
# next restart. Say so out loud: an RA who un-verifies a
|
| 100 |
+
# construct should be able to find out why it came back.
|
| 101 |
+
logger.warning(
|
| 102 |
+
"construct %s v%s: verification_status %s -> %s (from YAML %s)",
|
| 103 |
+
slug, version, existing.verification_status, new, c["_file"],
|
| 104 |
+
)
|
| 105 |
+
setattr(existing, f, new)
|
| 106 |
+
if changed:
|
| 107 |
+
report["updated"] += 1
|
| 108 |
+
report.setdefault("updated_detail", []).append(f"{slug}: {', '.join(changed)}")
|
| 109 |
+
else:
|
| 110 |
+
report["unchanged"] += 1
|
| 111 |
continue
|
| 112 |
|
| 113 |
db.add(
|
|
@@ -427,6 +427,26 @@ def run_job(job_id: str) -> None:
|
|
| 427 |
"the contrasting construct is a genuine opposite of the target.",
|
| 428 |
))
|
| 429 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
# Export mirrors ccr_wrapper's shape: input columns + per-item
|
| 431 |
# similarity columns + overall score, so it drops into existing
|
| 432 |
# CCR workflows. Multi-construct runs prefix each construct's columns
|
|
|
|
| 427 |
"the contrasting construct is a genuine opposite of the target.",
|
| 428 |
))
|
| 429 |
|
| 430 |
+
# Every item reverse-keyed flips what a high score means. v1 records the
|
| 431 |
+
# flags and does not adjust for them (adjustment_strategy: none), so the
|
| 432 |
+
# score is a similarity to items that all express the OPPOSITE of the
|
| 433 |
+
# construct's name. Say so, rather than leaving the results page to claim
|
| 434 |
+
# a higher score means more of the construct.
|
| 435 |
+
for c in ([*constructs, opposite] if anchored else constructs):
|
| 436 |
+
if not c:
|
| 437 |
+
continue
|
| 438 |
+
flags = json.loads(c.reverse_flags_json or "[]")
|
| 439 |
+
if flags and all(flags):
|
| 440 |
+
warnings.append(W(
|
| 441 |
+
"CONSTRUCT_ALL_ITEMS_REVERSED", "warning",
|
| 442 |
+
f"Every item in \"{c.name}\" is reverse-scored. Scores are raw "
|
| 443 |
+
"similarities with no reverse adjustment applied, so a HIGHER "
|
| 444 |
+
"score means the text expresses the opposite of this construct. "
|
| 445 |
+
"Interpret the direction accordingly, or reverse the sign yourself "
|
| 446 |
+
"downstream.",
|
| 447 |
+
construct=c.name,
|
| 448 |
+
))
|
| 449 |
+
|
| 450 |
# Export mirrors ccr_wrapper's shape: input columns + per-item
|
| 451 |
# similarity columns + overall score, so it drops into existing
|
| 452 |
# CCR workflows. Multi-construct runs prefix each construct's columns
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -19,7 +19,7 @@
|
|
| 19 |
property="og:description"
|
| 20 |
content="Score a text corpus against validated psychological questionnaires. Operated by the Culture and Morality Lab, University of Massachusetts Amherst."
|
| 21 |
/>
|
| 22 |
-
<script type="module" crossorigin src="/assets/index-
|
| 23 |
<link rel="stylesheet" crossorigin href="/assets/index-BGUc1PzI.css">
|
| 24 |
</head>
|
| 25 |
<body>
|
|
|
|
| 19 |
property="og:description"
|
| 20 |
content="Score a text corpus against validated psychological questionnaires. Operated by the Culture and Morality Lab, University of Massachusetts Amherst."
|
| 21 |
/>
|
| 22 |
+
<script type="module" crossorigin src="/assets/index-C8cIJp9I.js"></script>
|
| 23 |
<link rel="stylesheet" crossorigin href="/assets/index-BGUc1PzI.css">
|
| 24 |
</head>
|
| 25 |
<body>
|
|
@@ -12,6 +12,7 @@ Contract under test:
|
|
| 12 |
"""
|
| 13 |
|
| 14 |
import io
|
|
|
|
| 15 |
import time
|
| 16 |
|
| 17 |
import pytest
|
|
@@ -173,9 +174,9 @@ def test_review_applied_expected_shape():
|
|
| 173 |
for c in constructs:
|
| 174 |
by_status.setdefault(c["verification_status"], []).append(c)
|
| 175 |
|
| 176 |
-
assert len(by_status["archived"]) ==
|
| 177 |
-
assert len(by_status["verified"]) ==
|
| 178 |
-
assert len(by_status["needs_verification"]) ==
|
| 179 |
|
| 180 |
live = [c for c in constructs if c["verification_status"] != "archived"]
|
| 181 |
assert len({c["construct_id"] for c in live}) == 94, "one live version per construct"
|
|
@@ -185,18 +186,33 @@ def test_review_applied_expected_shape():
|
|
| 185 |
)
|
| 186 |
assert reverse == 96, "reverse flags after the review (was 35)"
|
| 187 |
|
| 188 |
-
#
|
| 189 |
pending = sorted(c["construct_id"] for c in by_status["needs_verification"])
|
| 190 |
assert pending == sorted(
|
| 191 |
[
|
|
|
|
| 192 |
"ipip_50_item_big_five_factor_markers_agreeableness",
|
| 193 |
"ipip_50_item_big_five_factor_markers_conscientiousness",
|
| 194 |
"ipip_50_item_big_five_factor_markers_emotional_stability_neuroticism",
|
| 195 |
"ipip_50_item_big_five_factor_markers_extraversion",
|
| 196 |
"ipip_50_item_big_five_factor_markers_intellect_imagination",
|
|
|
|
| 197 |
"k10",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
]
|
| 199 |
)
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
|
| 202 |
def test_every_live_construct_records_who_verified_it():
|
|
@@ -210,8 +226,44 @@ def test_every_live_construct_records_who_verified_it():
|
|
| 210 |
|
| 211 |
|
| 212 |
def test_superseded_files_keep_their_original_items():
|
| 213 |
-
"""Append-only means
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
superseded = [
|
| 216 |
c for c in load_yaml_constructs() if c["verification_status"] == "archived"
|
| 217 |
]
|
|
@@ -220,9 +272,70 @@ def test_superseded_files_keep_their_original_items():
|
|
| 220 |
newer = yaml.safe_load(
|
| 221 |
(CONSTRUCTS_DIR / f"{old['construct_id']}_v2.yaml").read_text()
|
| 222 |
)
|
| 223 |
-
assert newer["version"] == 2
|
| 224 |
-
assert old["version"] == 1
|
| 225 |
-
# something about the items really did change - that is why v2 exists
|
| 226 |
old_items = [(i["text"], bool(i.get("reverse_scored"))) for i in old["items"]]
|
| 227 |
new_items = [(i["text"], bool(i.get("reverse_scored"))) for i in newer["items"]]
|
| 228 |
assert old_items != new_items, f"{old['construct_id']}: v2 with identical items"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
"""
|
| 13 |
|
| 14 |
import io
|
| 15 |
+
import subprocess
|
| 16 |
import time
|
| 17 |
|
| 18 |
import pytest
|
|
|
|
| 174 |
for c in constructs:
|
| 175 |
by_status.setdefault(c["verification_status"], []).append(c)
|
| 176 |
|
| 177 |
+
assert len(by_status["archived"]) == 21, "superseded v1 files"
|
| 178 |
+
assert len(by_status["verified"]) == 81
|
| 179 |
+
assert len(by_status["needs_verification"]) == 13
|
| 180 |
|
| 181 |
live = [c for c in constructs if c["verification_status"] != "archived"]
|
| 182 |
assert len({c["construct_id"] for c in live}) == 94, "one live version per construct"
|
|
|
|
| 186 |
)
|
| 187 |
assert reverse == 96, "reverse flags after the review (was 35)"
|
| 188 |
|
| 189 |
+
# Everything still unverified is unverified for a recorded reason.
|
| 190 |
pending = sorted(c["construct_id"] for c in by_status["needs_verification"])
|
| 191 |
assert pending == sorted(
|
| 192 |
[
|
| 193 |
+
# PI decision: the "I" prefix CCR adds to IPIP stems
|
| 194 |
"ipip_50_item_big_five_factor_markers_agreeableness",
|
| 195 |
"ipip_50_item_big_five_factor_markers_conscientiousness",
|
| 196 |
"ipip_50_item_big_five_factor_markers_emotional_stability_neuroticism",
|
| 197 |
"ipip_50_item_big_five_factor_markers_extraversion",
|
| 198 |
"ipip_50_item_big_five_factor_markers_intellect_imagination",
|
| 199 |
+
# PI decision: restoring the shared K10 stem onto each item
|
| 200 |
"k10",
|
| 201 |
+
# reviewer's correction contradicts the cited publication
|
| 202 |
+
"mfq_fairness",
|
| 203 |
+
"team_psychological_safety_scale",
|
| 204 |
+
# reviewer reported an item-ORDER problem that is not applied
|
| 205 |
+
"cbi_work_related_burnout",
|
| 206 |
+
"rses",
|
| 207 |
+
# no reachable source on record, so "verified" cannot be claimed
|
| 208 |
+
"bas_2",
|
| 209 |
+
"cage_questionnaire",
|
| 210 |
+
"mfq_care",
|
| 211 |
]
|
| 212 |
)
|
| 213 |
+
for c in by_status["needs_verification"]:
|
| 214 |
+
assert (c.get("review") or {}).get("notes"), \
|
| 215 |
+
f"{c['construct_id']}: unverified without a recorded reason"
|
| 216 |
|
| 217 |
|
| 218 |
def test_every_live_construct_records_who_verified_it():
|
|
|
|
| 226 |
|
| 227 |
|
| 228 |
def test_superseded_files_keep_their_original_items():
|
| 229 |
+
"""Append-only means a published version's ITEMS are never rewritten.
|
| 230 |
+
|
| 231 |
+
Compares each tracked construct against the committed version in git rather
|
| 232 |
+
than against its own v2 - comparing v1 to v2 would pass even if v1's items
|
| 233 |
+
had been quietly edited, which is the exact failure this guards.
|
| 234 |
+
"""
|
| 235 |
+
repo = CONSTRUCTS_DIR.parents[2]
|
| 236 |
+
base = subprocess.run(
|
| 237 |
+
["git", "rev-parse", "HEAD"], cwd=repo, capture_output=True, text=True
|
| 238 |
+
)
|
| 239 |
+
if base.returncode != 0: # not a git checkout (e.g. a packaged install)
|
| 240 |
+
pytest.skip("not a git checkout")
|
| 241 |
+
|
| 242 |
+
checked = 0
|
| 243 |
+
for path in sorted(CONSTRUCTS_DIR.glob("*.yaml")):
|
| 244 |
+
rel = path.relative_to(repo)
|
| 245 |
+
show = subprocess.run(
|
| 246 |
+
["git", "show", f"HEAD:{rel.as_posix()}"], cwd=repo, capture_output=True, text=True
|
| 247 |
+
)
|
| 248 |
+
if show.returncode != 0:
|
| 249 |
+
continue # new file in this change, nothing committed to compare against
|
| 250 |
+
committed = yaml.safe_load(show.stdout)
|
| 251 |
+
current = yaml.safe_load(path.read_text())
|
| 252 |
+
if committed["version"] != current["version"]:
|
| 253 |
+
continue
|
| 254 |
+
assert [i["text"] for i in committed["items"]] == [
|
| 255 |
+
i["text"] for i in current["items"]
|
| 256 |
+
], f"{path.name}: item text changed under an existing version"
|
| 257 |
+
assert [bool(i.get("reverse_scored")) for i in committed["items"]] == [
|
| 258 |
+
bool(i.get("reverse_scored")) for i in current["items"]
|
| 259 |
+
], f"{path.name}: reverse flags changed under an existing version"
|
| 260 |
+
assert committed["language"] == current["language"], path.name
|
| 261 |
+
checked += 1
|
| 262 |
+
assert checked > 50, f"expected to check most of the library, only saw {checked}"
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def test_superseded_versions_really_differ_from_their_replacement():
|
| 266 |
+
"""A v2 that matches its v1 item-for-item would be pure version churn."""
|
| 267 |
superseded = [
|
| 268 |
c for c in load_yaml_constructs() if c["verification_status"] == "archived"
|
| 269 |
]
|
|
|
|
| 272 |
newer = yaml.safe_load(
|
| 273 |
(CONSTRUCTS_DIR / f"{old['construct_id']}_v2.yaml").read_text()
|
| 274 |
)
|
| 275 |
+
assert newer["version"] == 2 and old["version"] == 1
|
|
|
|
|
|
|
| 276 |
old_items = [(i["text"], bool(i.get("reverse_scored"))) for i in old["items"]]
|
| 277 |
new_items = [(i["text"], bool(i.get("reverse_scored"))) for i in newer["items"]]
|
| 278 |
assert old_items != new_items, f"{old['construct_id']}: v2 with identical items"
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def test_all_reversed_construct_warns_about_score_direction(client):
|
| 282 |
+
"""Flipping every item in a construct flips what a high score means; the run
|
| 283 |
+
must say so rather than leaving the results page to claim otherwise."""
|
| 284 |
+
listed = client.get("/api/constructs").json()
|
| 285 |
+
target = next(
|
| 286 |
+
c for c in listed
|
| 287 |
+
if c["is_seed"] and c["reverse_scored"] and all(c["reverse_scored"])
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
project = client.post("/api/projects", json={"name": "Reversed", "description": ""}).json()
|
| 291 |
+
corpus = client.post(
|
| 292 |
+
f"/api/projects/{project['id']}/corpora",
|
| 293 |
+
files={"file": ("c.csv", io.BytesIO(CSV.encode()), "application/octet-stream")},
|
| 294 |
+
).json()
|
| 295 |
+
job = client.post(
|
| 296 |
+
"/api/jobs",
|
| 297 |
+
json={
|
| 298 |
+
"project_id": project["id"],
|
| 299 |
+
"corpus_id": corpus["id"],
|
| 300 |
+
"construct_ids": [target["id"]],
|
| 301 |
+
"text_column": "text",
|
| 302 |
+
"model_name": "fake-deterministic",
|
| 303 |
+
},
|
| 304 |
+
).json()
|
| 305 |
+
done = wait_for_job(client, job["id"])
|
| 306 |
+
assert done["status"] == "completed"
|
| 307 |
+
|
| 308 |
+
summary = client.get(f"/api/jobs/{done['id']}/results").json()["summary"]
|
| 309 |
+
codes = [w["code"] for w in summary["warnings"]]
|
| 310 |
+
assert "CONSTRUCT_ALL_ITEMS_REVERSED" in codes, codes
|
| 311 |
+
msg = next(
|
| 312 |
+
w["message"] for w in summary["warnings"]
|
| 313 |
+
if w["code"] == "CONSTRUCT_ALL_ITEMS_REVERSED"
|
| 314 |
+
)
|
| 315 |
+
assert "opposite" in msg.lower()
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def test_normal_construct_does_not_warn_about_direction(client):
|
| 319 |
+
listed = client.get("/api/constructs").json()
|
| 320 |
+
normal = next(
|
| 321 |
+
c for c in listed if c["is_seed"] and not any(c["reverse_scored"])
|
| 322 |
+
)
|
| 323 |
+
project = client.post("/api/projects", json={"name": "Normal", "description": ""}).json()
|
| 324 |
+
corpus = client.post(
|
| 325 |
+
f"/api/projects/{project['id']}/corpora",
|
| 326 |
+
files={"file": ("c.csv", io.BytesIO(CSV.encode()), "application/octet-stream")},
|
| 327 |
+
).json()
|
| 328 |
+
job = client.post(
|
| 329 |
+
"/api/jobs",
|
| 330 |
+
json={
|
| 331 |
+
"project_id": project["id"],
|
| 332 |
+
"corpus_id": corpus["id"],
|
| 333 |
+
"construct_ids": [normal["id"]],
|
| 334 |
+
"text_column": "text",
|
| 335 |
+
"model_name": "fake-deterministic",
|
| 336 |
+
},
|
| 337 |
+
).json()
|
| 338 |
+
done = wait_for_job(client, job["id"])
|
| 339 |
+
summary = client.get(f"/api/jobs/{done['id']}/results").json()["summary"]
|
| 340 |
+
codes = [w["code"] for w in summary["warnings"]]
|
| 341 |
+
assert "CONSTRUCT_ALL_ITEMS_REVERSED" not in codes
|
|
@@ -34,21 +34,22 @@ because item text, item order, and reverse flags all feed `item_hash`:
|
|
| 34 |
|
| 35 |
- 61 reverse-scoring flags flipped `false -> true` across 14 constructs. No flag went
|
| 36 |
the other way; the library goes from 35 to 96 reverse-flagged items.
|
| 37 |
-
-
|
| 38 |
- 1 subscale regrouping: `scs_sf_self_judgment_1` moves to `scs_sf_over_identification`
|
| 39 |
per the SCS-SF coding key, changing the item list of both constructs.
|
| 40 |
|
| 41 |
-
|
| 42 |
append-only means old versions remain resolvable for runs that already used them.
|
| 43 |
|
| 44 |
Metadata-level changes do NOT feed `item_hash` and are therefore applied in place on
|
| 45 |
the existing version:
|
| 46 |
|
| 47 |
-
- `verification_status` promoted `needs_verification -> verified` for
|
| 48 |
-
- Citation and `source_url` repairs
|
| 49 |
-
|
| 50 |
- A `review:` provenance block on every construct recording reviewer, review date, and
|
| 51 |
-
outcome, so `verified` is attributable
|
|
|
|
| 52 |
|
| 53 |
**Loader changes.** Two gaps made the above impossible to ship as data alone:
|
| 54 |
|
|
@@ -68,12 +69,21 @@ the existing version:
|
|
| 68 |
**Warnings & edge cases.** Four constructs become entirely reverse-scored once the
|
| 69 |
reviewer's flags are applied: De Jong Gierveld social loneliness (3 items), and the
|
| 70 |
SCS-SF isolation, over-identification, and self-judgment subscales (2 items each).
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
**Metadata additions.** None. No output column changes, no `output_schema_version`
|
| 79 |
bump: scores, columns, and the export shape are unchanged. Runs that pin a v1
|
|
@@ -81,8 +91,38 @@ construct reproduce exactly as before, because v1 rows are never mutated.
|
|
| 81 |
|
| 82 |
## Non-goals
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
- **IPIP "I" prefix** (50 items across the 5 Big Five constructs). CCR prepends "I" to
|
| 88 |
the IPIP item stems; the reviewer confirmed the wording otherwise matches the source
|
|
@@ -104,31 +144,50 @@ similarities plus flags; `adjustment_strategy` remains a recorded parameter).
|
|
| 104 |
|
| 105 |
- `test_construct_library_versions.py::test_v2_constructs_supersede_v1_in_listing` -
|
| 106 |
the API lists one row per seed slug, at the highest version.
|
| 107 |
-
- `
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
## Implementation notes
|
| 118 |
|
| 119 |
-
Files changed: `packages/construct_library/constructs/*.yaml` (
|
| 120 |
in-place metadata updates), `packages/construct_library/apply_review.py` (new),
|
| 121 |
`packages/construct_library/reviews/` (new, the returned review file),
|
| 122 |
`backend/app/construct_lib.py` (metadata sync), `backend/app/main.py` (collapse to
|
| 123 |
-
latest seed version), `backend/
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
## Deviations (filled after implementation)
|
| 126 |
|
| 127 |
-
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
-
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
- 61 reverse-scoring flags flipped `false -> true` across 14 constructs. No flag went
|
| 36 |
the other way; the library goes from 35 to 96 reverse-flagged items.
|
| 37 |
+
- 7 verbatim wording corrections, one item each, in 7 constructs.
|
| 38 |
- 1 subscale regrouping: `scs_sf_self_judgment_1` moves to `scs_sf_over_identification`
|
| 39 |
per the SCS-SF coding key, changing the item list of both constructs.
|
| 40 |
|
| 41 |
+
21 constructs get a `version: 2` file. The `version: 1` files stay exactly as they are:
|
| 42 |
append-only means old versions remain resolvable for runs that already used them.
|
| 43 |
|
| 44 |
Metadata-level changes do NOT feed `item_hash` and are therefore applied in place on
|
| 45 |
the existing version:
|
| 46 |
|
| 47 |
+
- `verification_status` promoted `needs_verification -> verified` for 81 constructs.
|
| 48 |
+
- Citation and `source_url` repairs (dead links replaced with the URLs the reviewer
|
| 49 |
+
supplied, DOIs she supplied added).
|
| 50 |
- A `review:` provenance block on every construct recording reviewer, review date, and
|
| 51 |
+
outcome, plus a note wherever something is unresolved, so `verified` is attributable
|
| 52 |
+
rather than an unsourced claim and every unverified construct says why.
|
| 53 |
|
| 54 |
**Loader changes.** Two gaps made the above impossible to ship as data alone:
|
| 55 |
|
|
|
|
| 69 |
**Warnings & edge cases.** Four constructs become entirely reverse-scored once the
|
| 70 |
reviewer's flags are applied: De Jong Gierveld social loneliness (3 items), and the
|
| 71 |
SCS-SF isolation, over-identification, and self-judgment subscales (2 items each).
|
| 72 |
+
This is a faithful reading of the source scales - the De Jong Gierveld social loneliness
|
| 73 |
+
items are all positively worded, and the SCS-SF negative subscales are reverse-keyed in
|
| 74 |
+
the published coding key - so the flags are correct.
|
| 75 |
+
|
| 76 |
+
What that means at scoring time needed fixing, not just noting. `exclude_reversed` is a
|
| 77 |
+
design-doc strategy that is NOT implemented: runs record
|
| 78 |
+
`adjustment_strategy: "none"` and aggregate `mean_all_items`, so nothing was blocked and
|
| 79 |
+
no adjustment was applied. A construct whose items are all reverse-keyed therefore scores
|
| 80 |
+
in the opposite direction to its own name, while the results page told every run
|
| 81 |
+
"Higher = the text expresses the construct more strongly". This spec adds a
|
| 82 |
+
`CONSTRUCT_ALL_ITEMS_REVERSED` warning (severity `warning`, names the construct) emitted
|
| 83 |
+
whenever every flag on a scored construct is set, including the opposite pole of an
|
| 84 |
+
anchored run, and the results page now defers to that warning instead of asserting a
|
| 85 |
+
direction. The validator's existing all-reversed line still fires at build time; its
|
| 86 |
+
message mentions `exclude_reversed`, which remains aspirational.
|
| 87 |
|
| 88 |
**Metadata additions.** None. No output column changes, no `output_schema_version`
|
| 89 |
bump: scores, columns, and the export shape are unchanged. Runs that pin a v1
|
|
|
|
| 91 |
|
| 92 |
## Non-goals
|
| 93 |
|
| 94 |
+
13 constructs keep `verification_status: needs_verification`, each with the reason
|
| 95 |
+
recorded in its `review.notes`.
|
| 96 |
+
|
| 97 |
+
**Two of the reviewer's wording corrections are NOT applied, because each disagrees with
|
| 98 |
+
the publication its own construct cites.** Both were checked against the source:
|
| 99 |
+
|
| 100 |
+
- `mfq_fairness` item 3: she proposed "treated differently from others"; the published
|
| 101 |
+
MFQ30 reads "treated differently **than** others", which is what the library already
|
| 102 |
+
had.
|
| 103 |
+
- `team_psychological_safety_scale` item 4: she proposed "It is safe to take a risk **in**
|
| 104 |
+
this team", sourced from a third-party questionnaire (novopsych TPS-7); Edmondson
|
| 105 |
+
(1999), the cited paper and the recorded `source_url`, reads "on this team", which is
|
| 106 |
+
what the library already had. Her replacement `source_url` is not applied either - the
|
| 107 |
+
recorded Edmondson PDF resolves and contains the scale.
|
| 108 |
+
|
| 109 |
+
Both keep their published wording and are held back for someone to reconcile the
|
| 110 |
+
disagreement. Applying them would have moved a `verified` item away from its source,
|
| 111 |
+
which is the opposite of what this pass is for.
|
| 112 |
+
|
| 113 |
+
**Two item-ORDER findings are recorded but not applied.** For `rses` the reviewer maps
|
| 114 |
+
stored items 1,2,3,6,7,9,10 to source items 7,10,2,9,1,3,6, and for
|
| 115 |
+
`cbi_work_related_burnout` she maps stored 7..13 to source 11,13,12,7,8,9,10. Item order
|
| 116 |
+
feeds `item_hash` and decides what each `sim_item_N` export column refers to, so
|
| 117 |
+
reordering is a scoring-visible change that needs a decision rather than a silent fix.
|
| 118 |
+
Their reverse-scoring corrections ARE applied.
|
| 119 |
+
|
| 120 |
+
**Three constructs have no reachable source on record** (`bas_2`, `cage_questionnaire`,
|
| 121 |
+
`mfq_care`; `mfq_fairness` too). The reviewer checked what she could and flagged the
|
| 122 |
+
missing links; "verified verbatim against the original publication" cannot honestly be
|
| 123 |
+
claimed without one, so they stay unverified until a working source is on file.
|
| 124 |
+
|
| 125 |
+
**Two findings are left pending an explicit PI decision:**
|
| 126 |
|
| 127 |
- **IPIP "I" prefix** (50 items across the 5 Big Five constructs). CCR prepends "I" to
|
| 128 |
the IPIP item stems; the reviewer confirmed the wording otherwise matches the source
|
|
|
|
| 144 |
|
| 145 |
- `test_construct_library_versions.py::test_v2_constructs_supersede_v1_in_listing` -
|
| 146 |
the API lists one row per seed slug, at the highest version.
|
| 147 |
+
- `test_superseded_version_still_resolvable_and_runnable` - a superseded row is hidden
|
| 148 |
+
from the picker, still fetchable by id, and still completes a run whose metadata
|
| 149 |
+
snapshot reports version 1.
|
| 150 |
+
- `test_sync_updates_verification_status_in_place` - re-syncing after a status change
|
| 151 |
+
updates the existing row.
|
| 152 |
+
- `test_sync_still_refuses_item_change_under_same_version` - the append-only guard is
|
| 153 |
+
intact.
|
| 154 |
+
- `test_review_applied_expected_shape` - 21 v2 files, 81 verified, 13
|
| 155 |
+
needs_verification, 96 reverse flags, and every unverified construct carries a recorded
|
| 156 |
+
reason.
|
| 157 |
+
- `test_superseded_files_keep_their_original_items` - compares every tracked construct
|
| 158 |
+
against the committed copy in git, so an edit to a published version's items or flags
|
| 159 |
+
fails even though the v1/v2 pair would look consistent.
|
| 160 |
+
- `test_all_reversed_construct_warns_about_score_direction` /
|
| 161 |
+
`test_normal_construct_does_not_warn_about_direction` - the direction warning fires for
|
| 162 |
+
an all-reversed construct and stays silent otherwise.
|
| 163 |
+
- `validate_constructs.py` passes with 115 files and reports the 4 new all-reversed
|
| 164 |
+
warnings alongside the pre-existing `grit_s_consistency_of_interests`.
|
| 165 |
|
| 166 |
## Implementation notes
|
| 167 |
|
| 168 |
+
Files changed: `packages/construct_library/constructs/*.yaml` (21 new v2 files, 94
|
| 169 |
in-place metadata updates), `packages/construct_library/apply_review.py` (new),
|
| 170 |
`packages/construct_library/reviews/` (new, the returned review file),
|
| 171 |
`backend/app/construct_lib.py` (metadata sync), `backend/app/main.py` (collapse to
|
| 172 |
+
latest seed version), `backend/app/admin.py` (keep superseded rows out of the review
|
| 173 |
+
queue and its backlog count), `backend/app/jobs.py` (the direction warning),
|
| 174 |
+
`frontend/src/ResultsView.jsx` (defer to it),
|
| 175 |
+
`backend/tests/test_construct_library_versions.py` (new).
|
| 176 |
|
| 177 |
## Deviations (filled after implementation)
|
| 178 |
|
| 179 |
+
- Four PsycNET URLs the reviewer supplied carried an `auth_token` query parameter (a
|
| 180 |
+
per-session credential that expires and should not be committed). The token is
|
| 181 |
+
stripped and the base URL kept; where that leaves a login wall, the citation's DOI is
|
| 182 |
+
the durable pointer. For `dirty_dozen_*` this replaces a working ResearchGate link with
|
| 183 |
+
a paywalled one, which is a downgrade worth revisiting.
|
| 184 |
+
- Three DOIs were added (SWLS, horizontal collectivism, horizontal individualism). All
|
| 185 |
+
three appear verbatim in the reviewer's notes; none was looked up. An earlier draft of
|
| 186 |
+
this change also added a page range for the Triandis & Gelfand reference that she had
|
| 187 |
+
not supplied - it has been removed, and only the SWLS page range (which she did supply)
|
| 188 |
+
remains.
|
| 189 |
+
- The Grit-S replacement links are personal Dropbox share URLs and will rot. Kept because
|
| 190 |
+
the recorded links were dead, but they need a durable replacement.
|
| 191 |
+
- `mspss_significant_other` and `shs` wording corrections are applied on the reviewer's
|
| 192 |
+
word alone: neither recorded source served the item text for an independent check.
|
| 193 |
+
Every other applied wording change was confirmed against the cited publication.
|
|
@@ -65,7 +65,15 @@ export default function ResultsView({ jobId, onBack }) {
|
|
| 65 |
) : (
|
| 66 |
<>
|
| 67 |
CCR score = mean cosine similarity between each text and a construct's scale
|
| 68 |
-
items.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
{multi &&
|
| 70 |
" All constructs were scored on the same pass over the corpus, so scores are row-aligned and directly comparable."}
|
| 71 |
</>
|
|
|
|
| 65 |
) : (
|
| 66 |
<>
|
| 67 |
CCR score = mean cosine similarity between each text and a construct's scale
|
| 68 |
+
items.{" "}
|
| 69 |
+
{/* A construct whose items are ALL reverse-keyed scores in the
|
| 70 |
+
opposite direction, and the backend says so with
|
| 71 |
+
CONSTRUCT_ALL_ITEMS_REVERSED. Do not state the direction here
|
| 72 |
+
when that warning is present - the amber panel names which
|
| 73 |
+
construct is affected. */}
|
| 74 |
+
{summary.warnings?.some((w) => w.code === "CONSTRUCT_ALL_ITEMS_REVERSED")
|
| 75 |
+
? "Score direction depends on how each construct's items are keyed - see the warning below."
|
| 76 |
+
: "Higher = the text expresses the construct more strongly."}
|
| 77 |
{multi &&
|
| 78 |
" All constructs were scored on the same pass over the corpus, so scores are row-aligned and directly comparable."}
|
| 79 |
</>
|
|
@@ -50,6 +50,19 @@ PENDING = {
|
|
| 50 |
"ipip_50_item_big_five_factor_markers_extraversion",
|
| 51 |
"ipip_50_item_big_five_factor_markers_intellect_imagination",
|
| 52 |
"k10",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
}
|
| 54 |
PENDING_NOTE = {
|
| 55 |
"k10": (
|
|
@@ -57,6 +70,40 @@ PENDING_NOTE = {
|
|
| 57 |
"source stem 'During the last 30 days, about how often did you feel'. Pending PI "
|
| 58 |
"decision (spec 0007)."
|
| 59 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
IPIP_NOTE = (
|
| 62 |
"Reviewer confirms wording matches the source apart from the leading 'I' that CCR "
|
|
@@ -77,14 +124,19 @@ WORDING_FIX = {
|
|
| 77 |
'I often do "my own thing."',
|
| 78 |
"mfq_2_proportionality_3":
|
| 79 |
"I think people who are more hardworking should end up with more money.",
|
| 80 |
-
"fair_3":
|
| 81 |
-
"Whether or not some people were treated differently from others.",
|
| 82 |
"mspss_significant_other_2":
|
| 83 |
"There is a special person with whom I can share joys and sorrows.",
|
| 84 |
"shs_2":
|
| 85 |
"Compared to most of my peers, I consider myself:",
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
}
|
| 89 |
|
| 90 |
# One item moves subscale per the SCS-SF coding key. It is renumbered into the
|
|
@@ -116,8 +168,10 @@ SOURCE_URL_FIX = {
|
|
| 116 |
"https://www.dropbox.com/scl/fi/5fw2nbvvswu6jfb/8-item-Grit-4.pdf?rlkey=htfr2ngc17y027uv9ebnj53zm&dl=0",
|
| 117 |
"grit_s_perseverance_of_effort":
|
| 118 |
"https://www.dropbox.com/scl/fi/5fw2nbvvswu6jfb/8-item-Grit-4.pdf?rlkey=htfr2ngc17y027uv9ebnj53zm&dl=0",
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
| 121 |
"dirty_dozen_machiavellianism": "https://psycnet.apa.org/fulltext/2010-10892-021.pdf",
|
| 122 |
"dirty_dozen_narcissism": "https://psycnet.apa.org/fulltext/2010-10892-021.pdf",
|
| 123 |
"dirty_dozen_psychopathy": "https://psycnet.apa.org/fulltext/2010-10892-021.pdf",
|
|
@@ -126,21 +180,25 @@ SOURCE_URL_FIX = {
|
|
| 126 |
"individualism_horizontal": "https://psycnet.apa.org/fulltext/1997-38342-009.pdf",
|
| 127 |
}
|
| 128 |
|
| 129 |
-
# Citations the reviewer marked incomplete.
|
| 130 |
-
#
|
|
|
|
|
|
|
| 131 |
CITATION_FIX = {
|
|
|
|
| 132 |
"satisfaction_with_life":
|
| 133 |
"Diener, E., Emmons, R. A., Larsen, R. J., & Griffin, S. (1985). The Satisfaction "
|
| 134 |
"with Life Scale. Journal of Personality Assessment, 49(1), 71-75. "
|
| 135 |
"https://doi.org/10.1207/s15327752jpa4901_13",
|
|
|
|
| 136 |
"collectivism_horizontal":
|
| 137 |
"Triandis, H. C., & Gelfand, M. J. (1998). Converging measurement of horizontal and "
|
| 138 |
"vertical individualism and collectivism. Journal of Personality and Social "
|
| 139 |
-
"Psychology, 74(1)
|
| 140 |
"individualism_horizontal":
|
| 141 |
"Triandis, H. C., & Gelfand, M. J. (1998). Converging measurement of horizontal and "
|
| 142 |
"vertical individualism and collectivism. Journal of Personality and Social "
|
| 143 |
-
"Psychology, 74(1)
|
| 144 |
}
|
| 145 |
|
| 146 |
QUESTIONNAIRE_FIX = {
|
|
@@ -148,31 +206,39 @@ QUESTIONNAIRE_FIX = {
|
|
| 148 |
"individualism_horizontal": "Horizontal and Vertical Individualism and Collectivism Scale",
|
| 149 |
}
|
| 150 |
|
| 151 |
-
#
|
|
|
|
| 152 |
REVIEW_NOTE = {
|
| 153 |
-
"bas_2": "Reviewer could not reach the recorded source URL; wording checked against the "
|
| 154 |
-
"publisher PDF. A working source link is still needed.",
|
| 155 |
-
"cage_questionnaire": "Citation is correct but the recorded source URL is no longer "
|
| 156 |
-
"accessible; a working source link is still needed.",
|
| 157 |
-
"mfq_care": "Citation is correct; no source URL on file yet.",
|
| 158 |
-
"mfq_fairness": "Citation is correct; no source URL on file yet.",
|
| 159 |
"lot_r": "The scale's 4 filler items are correctly excluded from this construct; they are "
|
| 160 |
"not scored in the source.",
|
| 161 |
-
"cbi_work_related_burnout": "Reviewer notes the source numbers these items 7 to 13.",
|
| 162 |
}
|
| 163 |
|
| 164 |
|
| 165 |
def load_review() -> dict:
|
| 166 |
-
"""Read the reviewer's sheet. Returns {item_id: should_be_reverse_scored}.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
ws = openpyxl.load_workbook(REVIEW_XLSX, data_only=True)["Items"]
|
| 168 |
hdr = [c.value for c in ws[1]]
|
| 169 |
idx = {h: i for i, h in enumerate(hdr) if h}
|
| 170 |
-
flags = {}
|
| 171 |
for r in ws.iter_rows(min_row=2, values_only=True):
|
| 172 |
if not r[0]:
|
| 173 |
continue
|
| 174 |
item_id = str(r[idx["item_id"]]).strip()
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
return flags
|
| 177 |
|
| 178 |
|
|
@@ -190,7 +256,7 @@ def main() -> int:
|
|
| 190 |
|
| 191 |
review_flags = load_review()
|
| 192 |
files = sorted(CONSTRUCTS.glob("*.yaml"))
|
| 193 |
-
plan = {"new_version": [], "in_place": [], "unchanged": []}
|
| 194 |
|
| 195 |
for f in files:
|
| 196 |
c = yaml.safe_load(f.read_text())
|
|
@@ -205,9 +271,12 @@ def main() -> int:
|
|
| 205 |
for it in items:
|
| 206 |
iid = it["item_id"]
|
| 207 |
if iid in WORDING_FIX and it["text"] != WORDING_FIX[iid]:
|
|
|
|
| 208 |
it["text"] = WORDING_FIX[iid]
|
| 209 |
changed_items = True
|
| 210 |
if iid in review_flags and bool(it.get("reverse_scored", False)) != review_flags[iid]:
|
|
|
|
|
|
|
| 211 |
it["reverse_scored"] = review_flags[iid]
|
| 212 |
changed_items = True
|
| 213 |
|
|
@@ -278,13 +347,28 @@ def main() -> int:
|
|
| 278 |
"# Construct: versioned, append-only. Edits create a NEW version "
|
| 279 |
"(see registries rule).\n")
|
| 280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
live_total = len(plan["new_version"]) + len(plan["in_place"]) + len(plan["unchanged"])
|
|
|
|
| 282 |
print(f"new version 2 files : {len(plan['new_version'])}")
|
| 283 |
print(f"in-place metadata : {len(plan['in_place'])}")
|
| 284 |
print(f"already current : {len(plan['unchanged'])}")
|
| 285 |
print(f"\nlive constructs : {live_total}")
|
| 286 |
print(f" verified : {live_total - len(PENDING)}")
|
| 287 |
-
print(f"
|
|
|
|
|
|
|
| 288 |
if not write:
|
| 289 |
print("\nDry run. Re-run with --write to apply.")
|
| 290 |
return 0
|
|
|
|
| 50 |
"ipip_50_item_big_five_factor_markers_extraversion",
|
| 51 |
"ipip_50_item_big_five_factor_markers_intellect_imagination",
|
| 52 |
"k10",
|
| 53 |
+
# Reviewer's correction disagrees with the cited publication (see below).
|
| 54 |
+
"team_psychological_safety_scale",
|
| 55 |
+
"mfq_fairness",
|
| 56 |
+
# Reviewer reported an item-ORDER problem, which is not applied here. Order
|
| 57 |
+
# feeds item_hash and decides what sim_item_N means in an export, so it is
|
| 58 |
+
# not a cosmetic change to make without a decision.
|
| 59 |
+
"rses",
|
| 60 |
+
"cbi_work_related_burnout",
|
| 61 |
+
# No reachable source on record, so "verified verbatim against the original
|
| 62 |
+
# publication" cannot honestly be claimed.
|
| 63 |
+
"bas_2",
|
| 64 |
+
"cage_questionnaire",
|
| 65 |
+
"mfq_care",
|
| 66 |
}
|
| 67 |
PENDING_NOTE = {
|
| 68 |
"k10": (
|
|
|
|
| 70 |
"source stem 'During the last 30 days, about how often did you feel'. Pending PI "
|
| 71 |
"decision (spec 0007)."
|
| 72 |
),
|
| 73 |
+
"team_psychological_safety_scale": (
|
| 74 |
+
"Reviewer proposed 'It is safe to take a risk in this team' from a third-party "
|
| 75 |
+
"questionnaire (novopsych TPS-7). The cited source, Edmondson (1999), reads 'It is "
|
| 76 |
+
"safe to take a risk on this team' - the wording already stored here. Kept as "
|
| 77 |
+
"published; the two sources disagree and the citation should decide."
|
| 78 |
+
),
|
| 79 |
+
"mfq_fairness": (
|
| 80 |
+
"Reviewer proposed 'treated differently from others'. The published MFQ30 reads "
|
| 81 |
+
"'treated differently than others' - the wording already stored here. Kept as "
|
| 82 |
+
"published. No source_url on file yet."
|
| 83 |
+
),
|
| 84 |
+
"rses": (
|
| 85 |
+
"Reviewer maps stored items 1,2,3,6,7,9,10 to source item numbers 7,10,2,9,1,3,6: "
|
| 86 |
+
"the item ORDER does not match the source. Reverse-scoring corrections are applied; "
|
| 87 |
+
"the reordering is not, because order feeds item_hash and the sim_item_N export "
|
| 88 |
+
"columns. Pending a decision."
|
| 89 |
+
),
|
| 90 |
+
"cbi_work_related_burnout": (
|
| 91 |
+
"Reviewer maps stored items 7,8,9,10,11,12,13 to source item numbers "
|
| 92 |
+
"11,13,12,7,8,9,10: the item ORDER does not match the source. Not applied; order "
|
| 93 |
+
"feeds item_hash and the sim_item_N export columns. Pending a decision."
|
| 94 |
+
),
|
| 95 |
+
"bas_2": (
|
| 96 |
+
"Reviewer could not reach the recorded source URL and checked wording against the "
|
| 97 |
+
"publisher PDF; her note adds that BAS-2 has no reverse-scored items and is scored "
|
| 98 |
+
"by averaging all 10. A working source link is still needed before this can be "
|
| 99 |
+
"called verified."
|
| 100 |
+
),
|
| 101 |
+
"cage_questionnaire": (
|
| 102 |
+
"Citation is correct but the recorded source URL is no longer accessible. The "
|
| 103 |
+
"reviewer's wording correction ('ought to' -> 'should') is applied; a working "
|
| 104 |
+
"source link is still needed before this can be called verified."
|
| 105 |
+
),
|
| 106 |
+
"mfq_care": "Citation is correct; no source_url on file yet, so wording is unconfirmed.",
|
| 107 |
}
|
| 108 |
IPIP_NOTE = (
|
| 109 |
"Reviewer confirms wording matches the source apart from the leading 'I' that CCR "
|
|
|
|
| 124 |
'I often do "my own thing."',
|
| 125 |
"mfq_2_proportionality_3":
|
| 126 |
"I think people who are more hardworking should end up with more money.",
|
|
|
|
|
|
|
| 127 |
"mspss_significant_other_2":
|
| 128 |
"There is a special person with whom I can share joys and sorrows.",
|
| 129 |
"shs_2":
|
| 130 |
"Compared to most of my peers, I consider myself:",
|
| 131 |
+
# NOT applied, and deliberately so: two of the reviewer's wording corrections
|
| 132 |
+
# disagree with the publication each construct cites, so applying them would
|
| 133 |
+
# move a `verified` item AWAY from its source. Both are checked quotes:
|
| 134 |
+
# fair_3 - published MFQ30 reads "treated differently than others"
|
| 135 |
+
# (moralfoundations.org MFQ30), not "from others".
|
| 136 |
+
# team_psychological_safety_scale_4 - Edmondson (1999), the cited paper and
|
| 137 |
+
# the recorded source_url, reads "It is safe to take a risk on this
|
| 138 |
+
# team"; "in this team" comes from a third-party questionnaire.
|
| 139 |
+
# Both constructs are in PENDING with the discrepancy recorded.
|
| 140 |
}
|
| 141 |
|
| 142 |
# One item moves subscale per the SCS-SF coding key. It is renumbered into the
|
|
|
|
| 168 |
"https://www.dropbox.com/scl/fi/5fw2nbvvswu6jfb/8-item-Grit-4.pdf?rlkey=htfr2ngc17y027uv9ebnj53zm&dl=0",
|
| 169 |
"grit_s_perseverance_of_effort":
|
| 170 |
"https://www.dropbox.com/scl/fi/5fw2nbvvswu6jfb/8-item-Grit-4.pdf?rlkey=htfr2ngc17y027uv9ebnj53zm&dl=0",
|
| 171 |
+
# team_psychological_safety_scale is deliberately absent: the reviewer's
|
| 172 |
+
# replacement is a third-party questionnaire whose wording disagrees with the
|
| 173 |
+
# cited paper, and the recorded Edmondson PDF still resolves and still
|
| 174 |
+
# contains the scale.
|
| 175 |
"dirty_dozen_machiavellianism": "https://psycnet.apa.org/fulltext/2010-10892-021.pdf",
|
| 176 |
"dirty_dozen_narcissism": "https://psycnet.apa.org/fulltext/2010-10892-021.pdf",
|
| 177 |
"dirty_dozen_psychopathy": "https://psycnet.apa.org/fulltext/2010-10892-021.pdf",
|
|
|
|
| 180 |
"individualism_horizontal": "https://psycnet.apa.org/fulltext/1997-38342-009.pdf",
|
| 181 |
}
|
| 182 |
|
| 183 |
+
# Citations the reviewer marked incomplete. Every DOI and page range below appears
|
| 184 |
+
# verbatim in her notes; no bibliographic detail is looked up or inferred. The only
|
| 185 |
+
# editorial change is expanding the journal abbreviation "JPSP", which the existing
|
| 186 |
+
# citation already named.
|
| 187 |
CITATION_FIX = {
|
| 188 |
+
# Reviewer supplied the full reference including "49(1), 71-75" and the DOI.
|
| 189 |
"satisfaction_with_life":
|
| 190 |
"Diener, E., Emmons, R. A., Larsen, R. J., & Griffin, S. (1985). The Satisfaction "
|
| 191 |
"with Life Scale. Journal of Personality Assessment, 49(1), 71-75. "
|
| 192 |
"https://doi.org/10.1207/s15327752jpa4901_13",
|
| 193 |
+
# Reviewer supplied the DOI only, so no page range is added here.
|
| 194 |
"collectivism_horizontal":
|
| 195 |
"Triandis, H. C., & Gelfand, M. J. (1998). Converging measurement of horizontal and "
|
| 196 |
"vertical individualism and collectivism. Journal of Personality and Social "
|
| 197 |
+
"Psychology, 74(1). https://doi.org/10.1037/0022-3514.74.1.118",
|
| 198 |
"individualism_horizontal":
|
| 199 |
"Triandis, H. C., & Gelfand, M. J. (1998). Converging measurement of horizontal and "
|
| 200 |
"vertical individualism and collectivism. Journal of Personality and Social "
|
| 201 |
+
"Psychology, 74(1). https://doi.org/10.1037/0022-3514.74.1.118",
|
| 202 |
}
|
| 203 |
|
| 204 |
QUESTIONNAIRE_FIX = {
|
|
|
|
| 206 |
"individualism_horizontal": "Horizontal and Vertical Individualism and Collectivism Scale",
|
| 207 |
}
|
| 208 |
|
| 209 |
+
# Notes on constructs that DO reach `verified`. Gaps serious enough to hold a
|
| 210 |
+
# construct back live in PENDING_NOTE instead.
|
| 211 |
REVIEW_NOTE = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
"lot_r": "The scale's 4 filler items are correctly excluded from this construct; they are "
|
| 213 |
"not scored in the source.",
|
|
|
|
| 214 |
}
|
| 215 |
|
| 216 |
|
| 217 |
def load_review() -> dict:
|
| 218 |
+
"""Read the reviewer's sheet. Returns {item_id: should_be_reverse_scored}.
|
| 219 |
+
|
| 220 |
+
An unanswered cell is an error, not a False: treating a blank as "not
|
| 221 |
+
reverse-scored" would silently un-flag a reversed item, which is a scoring
|
| 222 |
+
change nobody asked for and nothing downstream would surface.
|
| 223 |
+
"""
|
| 224 |
ws = openpyxl.load_workbook(REVIEW_XLSX, data_only=True)["Items"]
|
| 225 |
hdr = [c.value for c in ws[1]]
|
| 226 |
idx = {h: i for i, h in enumerate(hdr) if h}
|
| 227 |
+
flags, bad = {}, []
|
| 228 |
for r in ws.iter_rows(min_row=2, values_only=True):
|
| 229 |
if not r[0]:
|
| 230 |
continue
|
| 231 |
item_id = str(r[idx["item_id"]]).strip()
|
| 232 |
+
answer = str(r[idx["REVERSE-SCORED? (Y/N)"]] or "").strip().upper()
|
| 233 |
+
if answer not in ("Y", "N"):
|
| 234 |
+
bad.append(f"{item_id}: reverse-scored cell is {answer or 'blank'!r}")
|
| 235 |
+
continue
|
| 236 |
+
flags[item_id] = answer == "Y"
|
| 237 |
+
if bad:
|
| 238 |
+
raise ValueError(
|
| 239 |
+
f"{len(bad)} item(s) have no usable reverse-scoring answer; resolve them with "
|
| 240 |
+
"the reviewer before applying:\n " + "\n ".join(bad[:20])
|
| 241 |
+
)
|
| 242 |
return flags
|
| 243 |
|
| 244 |
|
|
|
|
| 256 |
|
| 257 |
review_flags = load_review()
|
| 258 |
files = sorted(CONSTRUCTS.glob("*.yaml"))
|
| 259 |
+
plan = {"new_version": [], "in_place": [], "unchanged": [], "detail": []}
|
| 260 |
|
| 261 |
for f in files:
|
| 262 |
c = yaml.safe_load(f.read_text())
|
|
|
|
| 271 |
for it in items:
|
| 272 |
iid = it["item_id"]
|
| 273 |
if iid in WORDING_FIX and it["text"] != WORDING_FIX[iid]:
|
| 274 |
+
plan["detail"].append(f" {cid}/{iid}: text -> {WORDING_FIX[iid]!r}")
|
| 275 |
it["text"] = WORDING_FIX[iid]
|
| 276 |
changed_items = True
|
| 277 |
if iid in review_flags and bool(it.get("reverse_scored", False)) != review_flags[iid]:
|
| 278 |
+
arrow = "no -> REVERSE" if review_flags[iid] else "REVERSE -> no"
|
| 279 |
+
plan["detail"].append(f" {cid}/{iid}: {arrow}")
|
| 280 |
it["reverse_scored"] = review_flags[iid]
|
| 281 |
changed_items = True
|
| 282 |
|
|
|
|
| 347 |
"# Construct: versioned, append-only. Edits create a NEW version "
|
| 348 |
"(see registries rule).\n")
|
| 349 |
|
| 350 |
+
# A construct that no longer needs a new version must not leave one behind:
|
| 351 |
+
# a stale v2 would keep winning in the picker (newest version per slug).
|
| 352 |
+
stale = [
|
| 353 |
+
p for p in CONSTRUCTS.glob("*_v2.yaml")
|
| 354 |
+
if yaml.safe_load(p.read_text())["construct_id"] not in plan["new_version"]
|
| 355 |
+
]
|
| 356 |
+
for p in stale:
|
| 357 |
+
print(f"stale version 2 file (no longer needed): {p.name}")
|
| 358 |
+
if write:
|
| 359 |
+
p.unlink()
|
| 360 |
+
|
| 361 |
+
print("\n".join(plan["detail"]) if plan["detail"] else " (no item-level changes)")
|
| 362 |
live_total = len(plan["new_version"]) + len(plan["in_place"]) + len(plan["unchanged"])
|
| 363 |
+
print(f"\nitem-level changes : {len(plan['detail'])}")
|
| 364 |
print(f"new version 2 files : {len(plan['new_version'])}")
|
| 365 |
print(f"in-place metadata : {len(plan['in_place'])}")
|
| 366 |
print(f"already current : {len(plan['unchanged'])}")
|
| 367 |
print(f"\nlive constructs : {live_total}")
|
| 368 |
print(f" verified : {live_total - len(PENDING)}")
|
| 369 |
+
print(f" needs_verification: {len(PENDING)}")
|
| 370 |
+
for cid in sorted(PENDING):
|
| 371 |
+
print(f" {cid}")
|
| 372 |
if not write:
|
| 373 |
print("\nDry run. Re-run with --write to apply.")
|
| 374 |
return 0
|
|
@@ -7,7 +7,7 @@ category: Body appreciation / body image
|
|
| 7 |
description: Body appreciation / body image - Body Appreciation Scale-2 (BAS-2).
|
| 8 |
citation: 'Tylka, T. L., & Wood-Barcalow, N. L. (2015). The Body Appreciation Scale-2: Item refinement
|
| 9 |
and psychometric evaluation. Body Image, 12, 53–67. https://doi.org/10.1016/j.bodyim.2014.09.006'
|
| 10 |
-
verification_status:
|
| 11 |
source_url: https://emerge.ucsd.edu/r_4dsm5/body-appreciation-scale-2/
|
| 12 |
rights_note: Research center item page; verify permission for commercial redistribution.
|
| 13 |
questionnaire: Body Appreciation Scale-2 (BAS-2)
|
|
@@ -16,9 +16,10 @@ imported_from: lib.xlsx
|
|
| 16 |
review:
|
| 17 |
reviewer: Noor Skhiri
|
| 18 |
date: '2026-08-25'
|
| 19 |
-
outcome:
|
| 20 |
-
notes: Reviewer could not reach the recorded source URL
|
| 21 |
-
|
|
|
|
| 22 |
items:
|
| 23 |
- item_id: bas_2_1
|
| 24 |
text: I respect my body.
|
|
|
|
| 7 |
description: Body appreciation / body image - Body Appreciation Scale-2 (BAS-2).
|
| 8 |
citation: 'Tylka, T. L., & Wood-Barcalow, N. L. (2015). The Body Appreciation Scale-2: Item refinement
|
| 9 |
and psychometric evaluation. Body Image, 12, 53–67. https://doi.org/10.1016/j.bodyim.2014.09.006'
|
| 10 |
+
verification_status: needs_verification
|
| 11 |
source_url: https://emerge.ucsd.edu/r_4dsm5/body-appreciation-scale-2/
|
| 12 |
rights_note: Research center item page; verify permission for commercial redistribution.
|
| 13 |
questionnaire: Body Appreciation Scale-2 (BAS-2)
|
|
|
|
| 16 |
review:
|
| 17 |
reviewer: Noor Skhiri
|
| 18 |
date: '2026-08-25'
|
| 19 |
+
outcome: pending_pi_decision
|
| 20 |
+
notes: Reviewer could not reach the recorded source URL and checked wording against the publisher PDF;
|
| 21 |
+
her note adds that BAS-2 has no reverse-scored items and is scored by averaging all 10. A working
|
| 22 |
+
source link is still needed before this can be called verified.
|
| 23 |
items:
|
| 24 |
- item_id: bas_2_1
|
| 25 |
text: I respect my body.
|
|
@@ -7,7 +7,7 @@ category: Alcohol use risk
|
|
| 7 |
description: Alcohol use risk - CAGE Questionnaire. Screening tool only; not a diagnosis.
|
| 8 |
citation: 'Ewing, J. A. (1984). Detecting alcoholism: The CAGE questionnaire. JAMA, 252(14), 1905–1907.
|
| 9 |
https://doi.org/10.1001/jama.1984.03350140051025'
|
| 10 |
-
verification_status:
|
| 11 |
source_url: https://www.hopkinsmedicine.org/-/media/medicine/documents/cage-substance-screening-tool.pdf
|
| 12 |
rights_note: Institutional clinical handout; verify permission for redistribution. Screening tool only;
|
| 13 |
not a diagnosis.
|
|
@@ -17,9 +17,10 @@ imported_from: lib.xlsx
|
|
| 17 |
review:
|
| 18 |
reviewer: Noor Skhiri
|
| 19 |
date: '2026-08-25'
|
| 20 |
-
outcome:
|
| 21 |
-
notes: Citation is correct but the recorded source URL is no longer accessible
|
| 22 |
-
is still needed
|
|
|
|
| 23 |
items:
|
| 24 |
- item_id: cage_questionnaire_1
|
| 25 |
text: Have you ever felt you should Cut down on your drinking?
|
|
|
|
| 7 |
description: Alcohol use risk - CAGE Questionnaire. Screening tool only; not a diagnosis.
|
| 8 |
citation: 'Ewing, J. A. (1984). Detecting alcoholism: The CAGE questionnaire. JAMA, 252(14), 1905–1907.
|
| 9 |
https://doi.org/10.1001/jama.1984.03350140051025'
|
| 10 |
+
verification_status: needs_verification
|
| 11 |
source_url: https://www.hopkinsmedicine.org/-/media/medicine/documents/cage-substance-screening-tool.pdf
|
| 12 |
rights_note: Institutional clinical handout; verify permission for redistribution. Screening tool only;
|
| 13 |
not a diagnosis.
|
|
|
|
| 17 |
review:
|
| 18 |
reviewer: Noor Skhiri
|
| 19 |
date: '2026-08-25'
|
| 20 |
+
outcome: pending_pi_decision
|
| 21 |
+
notes: Citation is correct but the recorded source URL is no longer accessible. The reviewer's wording
|
| 22 |
+
correction ('ought to' -> 'should') is applied; a working source link is still needed before this
|
| 23 |
+
can be called verified.
|
| 24 |
items:
|
| 25 |
- item_id: cage_questionnaire_1
|
| 26 |
text: Have you ever felt you should Cut down on your drinking?
|
|
@@ -7,7 +7,7 @@ category: Burnout
|
|
| 7 |
description: Burnout - Work-related burnout dimension of the Copenhagen Burnout Inventory (CBI).
|
| 8 |
citation: 'Kristensen, T. S., Borritz, M., Villadsen, E., & Christensen, K. B. (2005). The Copenhagen
|
| 9 |
Burnout Inventory: A new tool for the assessment of burnout. Work & Stress, 19(3), 192–207. https://doi.org/10.1080/02678370500297720'
|
| 10 |
-
verification_status:
|
| 11 |
source_url: https://emerge.ucsd.edu/r_2qfb6wi4uepyugd/
|
| 12 |
rights_note: Research center item page; verify permission for commercial redistribution.
|
| 13 |
questionnaire: Copenhagen Burnout Inventory (CBI)
|
|
@@ -16,8 +16,10 @@ imported_from: lib.xlsx
|
|
| 16 |
review:
|
| 17 |
reviewer: Noor Skhiri
|
| 18 |
date: '2026-08-25'
|
| 19 |
-
outcome:
|
| 20 |
-
notes: Reviewer
|
|
|
|
|
|
|
| 21 |
items:
|
| 22 |
- item_id: cbi_work_related_burnout_7
|
| 23 |
text: Is your work emotionally exhausting?
|
|
|
|
| 7 |
description: Burnout - Work-related burnout dimension of the Copenhagen Burnout Inventory (CBI).
|
| 8 |
citation: 'Kristensen, T. S., Borritz, M., Villadsen, E., & Christensen, K. B. (2005). The Copenhagen
|
| 9 |
Burnout Inventory: A new tool for the assessment of burnout. Work & Stress, 19(3), 192–207. https://doi.org/10.1080/02678370500297720'
|
| 10 |
+
verification_status: needs_verification
|
| 11 |
source_url: https://emerge.ucsd.edu/r_2qfb6wi4uepyugd/
|
| 12 |
rights_note: Research center item page; verify permission for commercial redistribution.
|
| 13 |
questionnaire: Copenhagen Burnout Inventory (CBI)
|
|
|
|
| 16 |
review:
|
| 17 |
reviewer: Noor Skhiri
|
| 18 |
date: '2026-08-25'
|
| 19 |
+
outcome: pending_pi_decision
|
| 20 |
+
notes: 'Reviewer maps stored items 7,8,9,10,11,12,13 to source item numbers 11,13,12,7,8,9,10: the item
|
| 21 |
+
ORDER does not match the source. Not applied; order feeds item_hash and the sim_item_N export columns.
|
| 22 |
+
Pending a decision.'
|
| 23 |
items:
|
| 24 |
- item_id: cbi_work_related_burnout_7
|
| 25 |
text: Is your work emotionally exhausting?
|
|
@@ -6,7 +6,7 @@ language: en
|
|
| 6 |
category: cultural-orientation
|
| 7 |
description: Interdependence, cooperation, and in-group well-being (Triandis & Gelfand horizontal collectivism).
|
| 8 |
citation: Triandis, H. C., & Gelfand, M. J. (1998). Converging measurement of horizontal and vertical
|
| 9 |
-
individualism and collectivism. Journal of Personality and Social Psychology, 74(1)
|
| 10 |
verification_status: verified
|
| 11 |
source_url: https://psycnet.apa.org/fulltext/1997-38342-009.pdf
|
| 12 |
questionnaire: Horizontal and Vertical Individualism and Collectivism Scale
|
|
|
|
| 6 |
category: cultural-orientation
|
| 7 |
description: Interdependence, cooperation, and in-group well-being (Triandis & Gelfand horizontal collectivism).
|
| 8 |
citation: Triandis, H. C., & Gelfand, M. J. (1998). Converging measurement of horizontal and vertical
|
| 9 |
+
individualism and collectivism. Journal of Personality and Social Psychology, 74(1). https://doi.org/10.1037/0022-3514.74.1.118
|
| 10 |
verification_status: verified
|
| 11 |
source_url: https://psycnet.apa.org/fulltext/1997-38342-009.pdf
|
| 12 |
questionnaire: Horizontal and Vertical Individualism and Collectivism Scale
|
|
@@ -6,7 +6,7 @@ language: en
|
|
| 6 |
category: cultural-orientation
|
| 7 |
description: Self-reliance and independence from in-groups (Triandis & Gelfand horizontal individualism).
|
| 8 |
citation: Triandis, H. C., & Gelfand, M. J. (1998). Converging measurement of horizontal and vertical
|
| 9 |
-
individualism and collectivism. Journal of Personality and Social Psychology, 74(1)
|
| 10 |
verification_status: verified
|
| 11 |
review:
|
| 12 |
reviewer: Noor Skhiri
|
|
|
|
| 6 |
category: cultural-orientation
|
| 7 |
description: Self-reliance and independence from in-groups (Triandis & Gelfand horizontal individualism).
|
| 8 |
citation: Triandis, H. C., & Gelfand, M. J. (1998). Converging measurement of horizontal and vertical
|
| 9 |
+
individualism and collectivism. Journal of Personality and Social Psychology, 74(1). https://doi.org/10.1037/0022-3514.74.1.118
|
| 10 |
verification_status: verified
|
| 11 |
review:
|
| 12 |
reviewer: Noor Skhiri
|
|
@@ -7,12 +7,12 @@ category: moral-foundations
|
|
| 7 |
description: Concern with suffering, compassion, and protection of the vulnerable (MFQ Care/Harm).
|
| 8 |
citation: Graham, J., Nosek, B. A., Haidt, J., Iyer, R., Koleva, S., & Ditto, P. H. (2011). Mapping the
|
| 9 |
moral domain. JPSP, 101(2).
|
| 10 |
-
verification_status:
|
| 11 |
review:
|
| 12 |
reviewer: Noor Skhiri
|
| 13 |
date: '2026-08-25'
|
| 14 |
-
outcome:
|
| 15 |
-
notes: Citation is correct; no
|
| 16 |
items:
|
| 17 |
- item_id: care_1
|
| 18 |
text: Compassion for those who are suffering is the most crucial virtue.
|
|
|
|
| 7 |
description: Concern with suffering, compassion, and protection of the vulnerable (MFQ Care/Harm).
|
| 8 |
citation: Graham, J., Nosek, B. A., Haidt, J., Iyer, R., Koleva, S., & Ditto, P. H. (2011). Mapping the
|
| 9 |
moral domain. JPSP, 101(2).
|
| 10 |
+
verification_status: needs_verification
|
| 11 |
review:
|
| 12 |
reviewer: Noor Skhiri
|
| 13 |
date: '2026-08-25'
|
| 14 |
+
outcome: pending_pi_decision
|
| 15 |
+
notes: Citation is correct; no source_url on file yet, so wording is unconfirmed.
|
| 16 |
items:
|
| 17 |
- item_id: care_1
|
| 18 |
text: Compassion for those who are suffering is the most crucial virtue.
|
|
@@ -1,5 +1,4 @@
|
|
| 1 |
-
#
|
| 2 |
-
# version still resolve; never edit a published version in place.
|
| 3 |
construct_id: mfq_fairness
|
| 4 |
version: 1
|
| 5 |
name: Moral Foundations - Fairness
|
|
@@ -8,11 +7,13 @@ category: moral-foundations
|
|
| 8 |
description: Concern with justice, rights, and equal treatment (MFQ Fairness/Cheating).
|
| 9 |
citation: Graham, J., Nosek, B. A., Haidt, J., Iyer, R., Koleva, S., & Ditto, P. H. (2011). Mapping the
|
| 10 |
moral domain. JPSP, 101(2).
|
| 11 |
-
verification_status:
|
| 12 |
review:
|
| 13 |
reviewer: Noor Skhiri
|
| 14 |
date: '2026-08-25'
|
| 15 |
-
outcome:
|
|
|
|
|
|
|
| 16 |
items:
|
| 17 |
- item_id: fair_1
|
| 18 |
text: Justice is the most important requirement for a society.
|
|
|
|
| 1 |
+
# Construct: versioned, append-only. Edits create a NEW version (see registries rule).
|
|
|
|
| 2 |
construct_id: mfq_fairness
|
| 3 |
version: 1
|
| 4 |
name: Moral Foundations - Fairness
|
|
|
|
| 7 |
description: Concern with justice, rights, and equal treatment (MFQ Fairness/Cheating).
|
| 8 |
citation: Graham, J., Nosek, B. A., Haidt, J., Iyer, R., Koleva, S., & Ditto, P. H. (2011). Mapping the
|
| 9 |
moral domain. JPSP, 101(2).
|
| 10 |
+
verification_status: needs_verification
|
| 11 |
review:
|
| 12 |
reviewer: Noor Skhiri
|
| 13 |
date: '2026-08-25'
|
| 14 |
+
outcome: pending_pi_decision
|
| 15 |
+
notes: Reviewer proposed 'treated differently from others'. The published MFQ30 reads 'treated differently
|
| 16 |
+
than others' - the wording already stored here. Kept as published. No source_url on file yet.
|
| 17 |
items:
|
| 18 |
- item_id: fair_1
|
| 19 |
text: Justice is the most important requirement for a society.
|
|
@@ -1,30 +0,0 @@
|
|
| 1 |
-
# Construct: versioned, append-only. Version 2 applies the 2026-08-25 library review (spec 0007).
|
| 2 |
-
construct_id: mfq_fairness
|
| 3 |
-
version: 2
|
| 4 |
-
name: Moral Foundations - Fairness
|
| 5 |
-
language: en
|
| 6 |
-
category: moral-foundations
|
| 7 |
-
description: Concern with justice, rights, and equal treatment (MFQ Fairness/Cheating).
|
| 8 |
-
citation: Graham, J., Nosek, B. A., Haidt, J., Iyer, R., Koleva, S., & Ditto, P. H. (2011). Mapping the
|
| 9 |
-
moral domain. JPSP, 101(2).
|
| 10 |
-
verification_status: verified
|
| 11 |
-
review:
|
| 12 |
-
reviewer: Noor Skhiri
|
| 13 |
-
date: '2026-08-25'
|
| 14 |
-
outcome: verified
|
| 15 |
-
notes: Citation is correct; no source URL on file yet.
|
| 16 |
-
reverse_flags_source: reviewed_2026-08-25_noor_skhiri
|
| 17 |
-
items:
|
| 18 |
-
- item_id: fair_1
|
| 19 |
-
text: Justice is the most important requirement for a society.
|
| 20 |
-
reverse_scored: false
|
| 21 |
-
- item_id: fair_2
|
| 22 |
-
text: When the government makes laws, the number one principle should be ensuring that everyone is treated
|
| 23 |
-
fairly.
|
| 24 |
-
reverse_scored: false
|
| 25 |
-
- item_id: fair_3
|
| 26 |
-
text: Whether or not some people were treated differently from others.
|
| 27 |
-
reverse_scored: false
|
| 28 |
-
- item_id: fair_4
|
| 29 |
-
text: Whether or not someone acted unfairly.
|
| 30 |
-
reverse_scored: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -6,7 +6,7 @@ language: en
|
|
| 6 |
category: Self-esteem
|
| 7 |
description: Self-esteem - Rosenberg Self-Esteem Scale (RSES).
|
| 8 |
citation: Rosenberg, M. (1965). Society and the adolescent self-image. Princeton University Press.
|
| 9 |
-
verification_status:
|
| 10 |
source_url: https://socy.umd.edu/about-us/rosenberg-self-esteem-scale
|
| 11 |
rights_note: University of Maryland Rosenberg page states the RSES is public domain.
|
| 12 |
questionnaire: Rosenberg Self-Esteem Scale (RSES)
|
|
@@ -15,7 +15,10 @@ imported_from: lib.xlsx
|
|
| 15 |
review:
|
| 16 |
reviewer: Noor Skhiri
|
| 17 |
date: '2026-08-25'
|
| 18 |
-
outcome:
|
|
|
|
|
|
|
|
|
|
| 19 |
items:
|
| 20 |
- item_id: rses_1
|
| 21 |
text: On the whole, I am satisfied with myself.
|
|
|
|
| 6 |
category: Self-esteem
|
| 7 |
description: Self-esteem - Rosenberg Self-Esteem Scale (RSES).
|
| 8 |
citation: Rosenberg, M. (1965). Society and the adolescent self-image. Princeton University Press.
|
| 9 |
+
verification_status: needs_verification
|
| 10 |
source_url: https://socy.umd.edu/about-us/rosenberg-self-esteem-scale
|
| 11 |
rights_note: University of Maryland Rosenberg page states the RSES is public domain.
|
| 12 |
questionnaire: Rosenberg Self-Esteem Scale (RSES)
|
|
|
|
| 15 |
review:
|
| 16 |
reviewer: Noor Skhiri
|
| 17 |
date: '2026-08-25'
|
| 18 |
+
outcome: pending_pi_decision
|
| 19 |
+
notes: 'Reviewer maps stored items 1,2,3,6,7,9,10 to source item numbers 7,10,2,9,1,3,6: the item ORDER
|
| 20 |
+
does not match the source. Reverse-scoring corrections are applied; the reordering is not, because
|
| 21 |
+
order feeds item_hash and the sim_item_N export columns. Pending a decision.'
|
| 22 |
items:
|
| 23 |
- item_id: rses_1
|
| 24 |
text: On the whole, I am satisfied with myself.
|
|
@@ -1,5 +1,4 @@
|
|
| 1 |
-
#
|
| 2 |
-
# version still resolve; never edit a published version in place.
|
| 3 |
construct_id: team_psychological_safety_scale
|
| 4 |
version: 1
|
| 5 |
name: Team Psychological Safety Scale
|
|
@@ -8,16 +7,19 @@ category: Psychological safety
|
|
| 8 |
description: Psychological safety - Team Psychological Safety Scale.
|
| 9 |
citation: Edmondson, A. (1999). Psychological safety and learning behavior in work teams. Administrative
|
| 10 |
Science Quarterly, 44(2), 350–383. https://doi.org/10.2307/2666999
|
| 11 |
-
verification_status:
|
| 12 |
source_url: https://web.mit.edu/curhan/www/docs/Articles/15341_Readings/Group_Performance/Edmondson%20Psychological%20safety.pdf
|
| 13 |
rights_note: Author paper/public PDF includes scale; verify permission for commercial redistribution.
|
| 14 |
questionnaire: Team Psychological Safety Scale
|
| 15 |
-
reverse_flags_source:
|
| 16 |
imported_from: lib.xlsx
|
| 17 |
review:
|
| 18 |
reviewer: Noor Skhiri
|
| 19 |
date: '2026-08-25'
|
| 20 |
-
outcome:
|
|
|
|
|
|
|
|
|
|
| 21 |
items:
|
| 22 |
- item_id: team_psychological_safety_scale_1
|
| 23 |
text: If you make a mistake on this team, it is often held against you.
|
|
|
|
| 1 |
+
# Construct: versioned, append-only. Edits create a NEW version (see registries rule).
|
|
|
|
| 2 |
construct_id: team_psychological_safety_scale
|
| 3 |
version: 1
|
| 4 |
name: Team Psychological Safety Scale
|
|
|
|
| 7 |
description: Psychological safety - Team Psychological Safety Scale.
|
| 8 |
citation: Edmondson, A. (1999). Psychological safety and learning behavior in work teams. Administrative
|
| 9 |
Science Quarterly, 44(2), 350–383. https://doi.org/10.2307/2666999
|
| 10 |
+
verification_status: needs_verification
|
| 11 |
source_url: https://web.mit.edu/curhan/www/docs/Articles/15341_Readings/Group_Performance/Edmondson%20Psychological%20safety.pdf
|
| 12 |
rights_note: Author paper/public PDF includes scale; verify permission for commercial redistribution.
|
| 13 |
questionnaire: Team Psychological Safety Scale
|
| 14 |
+
reverse_flags_source: reviewed_2026-08-25_noor_skhiri
|
| 15 |
imported_from: lib.xlsx
|
| 16 |
review:
|
| 17 |
reviewer: Noor Skhiri
|
| 18 |
date: '2026-08-25'
|
| 19 |
+
outcome: pending_pi_decision
|
| 20 |
+
notes: Reviewer proposed 'It is safe to take a risk in this team' from a third-party questionnaire (novopsych
|
| 21 |
+
TPS-7). The cited source, Edmondson (1999), reads 'It is safe to take a risk on this team' - the wording
|
| 22 |
+
already stored here. Kept as published; the two sources disagree and the citation should decide.
|
| 23 |
items:
|
| 24 |
- item_id: team_psychological_safety_scale_1
|
| 25 |
text: If you make a mistake on this team, it is often held against you.
|
|
@@ -1,41 +0,0 @@
|
|
| 1 |
-
# Construct: versioned, append-only. Version 2 applies the 2026-08-25 library review (spec 0007).
|
| 2 |
-
construct_id: team_psychological_safety_scale
|
| 3 |
-
version: 2
|
| 4 |
-
name: Team Psychological Safety Scale
|
| 5 |
-
language: en
|
| 6 |
-
category: Psychological safety
|
| 7 |
-
description: Psychological safety - Team Psychological Safety Scale.
|
| 8 |
-
citation: Edmondson, A. (1999). Psychological safety and learning behavior in work teams. Administrative
|
| 9 |
-
Science Quarterly, 44(2), 350–383. https://doi.org/10.2307/2666999
|
| 10 |
-
verification_status: verified
|
| 11 |
-
source_url: https://novopsych.com/wp-content/uploads/2025/08/TPS-7-questionnaire.pdf
|
| 12 |
-
rights_note: Author paper/public PDF includes scale; verify permission for commercial redistribution.
|
| 13 |
-
questionnaire: Team Psychological Safety Scale
|
| 14 |
-
reverse_flags_source: reviewed_2026-08-25_noor_skhiri
|
| 15 |
-
imported_from: lib.xlsx
|
| 16 |
-
review:
|
| 17 |
-
reviewer: Noor Skhiri
|
| 18 |
-
date: '2026-08-25'
|
| 19 |
-
outcome: verified
|
| 20 |
-
items:
|
| 21 |
-
- item_id: team_psychological_safety_scale_1
|
| 22 |
-
text: If you make a mistake on this team, it is often held against you.
|
| 23 |
-
reverse_scored: false
|
| 24 |
-
- item_id: team_psychological_safety_scale_2
|
| 25 |
-
text: Members of this team are able to bring up problems and tough issues.
|
| 26 |
-
reverse_scored: false
|
| 27 |
-
- item_id: team_psychological_safety_scale_3
|
| 28 |
-
text: People on this team sometimes reject others for being different.
|
| 29 |
-
reverse_scored: false
|
| 30 |
-
- item_id: team_psychological_safety_scale_4
|
| 31 |
-
text: It is safe to take a risk in this team.
|
| 32 |
-
reverse_scored: false
|
| 33 |
-
- item_id: team_psychological_safety_scale_5
|
| 34 |
-
text: It is difficult to ask other members of this team for help.
|
| 35 |
-
reverse_scored: false
|
| 36 |
-
- item_id: team_psychological_safety_scale_6
|
| 37 |
-
text: No one on this team would deliberately act in a way that undermines my efforts.
|
| 38 |
-
reverse_scored: false
|
| 39 |
-
- item_id: team_psychological_safety_scale_7
|
| 40 |
-
text: Working with members of this team, my unique skills and talents are valued and utilized.
|
| 41 |
-
reverse_scored: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -59,7 +59,13 @@ def main() -> int:
|
|
| 59 |
seen[key] = f
|
| 60 |
|
| 61 |
if all(i.get("reverse_scored") for i in c["items"]):
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
if not errors or all(name not in e for e in errors):
|
| 65 |
print(f" OK {name} v{c['version']} items={len(c['items'])} hash={item_hash(c)[:16]} [{c['verification_status']}]")
|
|
|
|
| 59 |
seen[key] = f
|
| 60 |
|
| 61 |
if all(i.get("reverse_scored") for i in c["items"]):
|
| 62 |
+
# v1 records reverse flags and does not adjust for them
|
| 63 |
+
# (adjustment_strategy: none), so a run on this construct scores in
|
| 64 |
+
# the OPPOSITE direction to its name. Runs say so at scoring time via
|
| 65 |
+
# CONSTRUCT_ALL_ITEMS_REVERSED; the design doc's exclude_reversed
|
| 66 |
+
# strategy, which would block the aggregate, is not implemented yet.
|
| 67 |
+
print(f" WARN {name}: ALL items reverse-scored - a higher score means the text "
|
| 68 |
+
"expresses the OPPOSITE of this construct (no reverse adjustment is applied).")
|
| 69 |
|
| 70 |
if not errors or all(name not in e for e in errors):
|
| 71 |
print(f" OK {name} v{c['version']} items={len(c['items'])} hash={item_hash(c)[:16]} [{c['verification_status']}]")
|