devaanand Claude Opus 5 (1M context) commited on
Commit
461b859
·
1 Parent(s): 1b10b34

fix(constructs): correct the review pass after a validity review

Browse files

A 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 CHANGED
@@ -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); 88 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), 9 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. Six constructs (the five IPIP
17
- Big Five scales and the K10) stay flagged pending two wording decisions. Four
18
- constructs are now entirely reverse-scored, which blocks their aggregate under the
19
- exclude-reversed strategy. (spec 0007)
 
 
 
 
 
 
 
 
 
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.
README.md CHANGED
@@ -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
- source publications in August 2026: 88 constructs are marked verified and carry the
58
- reviewer and review date; the remaining 6 are still flagged in the interface pending
59
- two open wording decisions (see `docs/specs/0007-construct-library-verification-pass.md`).
60
- - Tests: `cd backend && CCR_FAKE_EMBEDDINGS=1 python -m pytest -q` (134 tests, no ML
 
 
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
ROADMAP.md CHANGED
@@ -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; 88 of 94 constructs verified. Two
56
- wording decisions still need the PI: the IPIP "I" prefix (50 items,
57
- 5 constructs) and restoring the K10 stem (10 items).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
backend/app/construct_lib.py CHANGED
@@ -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
- setattr(existing, f, _MUTABLE_FIELDS[f](c))
97
- report["updated" if changed else "unchanged"] += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
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(
backend/app/jobs.py CHANGED
@@ -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
backend/static/assets/{index-CCWGjEOt.js → index-C8cIJp9I.js} RENAMED
The diff for this file is too large to render. See raw diff
 
backend/static/index.html CHANGED
@@ -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-CCWGjEOt.js"></script>
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>
backend/tests/test_construct_library_versions.py CHANGED
@@ -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"]) == 23, "superseded v1 files"
177
- assert len(by_status["verified"]) == 88
178
- assert len(by_status["needs_verification"]) == 6
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
- # the pending decisions are exactly the ones the spec names
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 the old version's ITEMS are never rewritten - only its
214
- status and review provenance may change."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
docs/specs/0007-construct-library-verification-pass.md CHANGED
@@ -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
- - 9 verbatim wording corrections, one item each, in 9 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
- 23 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 88 constructs.
48
- - Citation and `source_url` repairs across 13 metadata-only constructs (dead links
49
- replaced with the URLs the reviewer supplied, one missing DOI added).
50
  - A `review:` provenance block on every construct recording reviewer, review date, and
51
- outcome, so `verified` is attributable rather than an unsourced claim.
 
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
- The existing validator warning fires for each, and under `exclude_reversed` their
72
- aggregate is blocked. This is a faithful reading of the source scales - the De Jong
73
- Gierveld social loneliness items are all positively worded, and the SCS-SF negative
74
- subscales are reverse-keyed in the published coding key - so the flags are correct and
75
- the warning is doing its job. It is called out here because it changes what those four
76
- constructs do at scoring time.
 
 
 
 
 
 
 
 
 
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
- Two findings are left pending an explicit PI decision and keep
85
- `verification_status: needs_verification`:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- - `test_construct_library_versions.py::test_superseded_version_still_resolvable_by_id` -
108
- a superseded row is still fetchable and usable for a run.
109
- - `test_construct_library_versions.py::test_sync_updates_verification_status_in_place` -
110
- re-syncing after a status change updates the existing row.
111
- - `test_construct_library_versions.py::test_sync_still_refuses_item_change_under_same_version` -
112
- the append-only guard is intact.
113
- - `test_construct_library_versions.py::test_review_applied_expected_shape` - 23 v2
114
- files exist, 88 constructs verified, 6 still needs_verification, 96 reverse flags.
115
- - `validate_constructs.py` passes with 117 files and reports the 4 all-reversed warnings.
 
 
 
 
 
 
 
 
 
116
 
117
  ## Implementation notes
118
 
119
- Files changed: `packages/construct_library/constructs/*.yaml` (23 new v2 files, 94
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/tests/test_construct_library_versions.py` (new).
 
 
 
124
 
125
  ## Deviations (filled after implementation)
126
 
127
- - Three source URLs the reviewer supplied carried a PsycNET `auth_token` query
128
- parameter (a per-session credential that expires and should not be committed). The
129
- token is stripped and the base URL kept; where that leaves a paywalled link, the
130
- citation's DOI remains the durable pointer. No DOI was invented: the only DOI added
131
- is the one the reviewer supplied verbatim for SWLS.
132
- - `bas_2` and `cage_questionnaire` were flagged "source URL not found / no longer
133
- accessible" with no replacement given. Their `source_url` is left unchanged and the
134
- gap is recorded in the construct's `review.notes` rather than guessed at.
 
 
 
 
 
 
 
 
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.
frontend/src/ResultsView.jsx CHANGED
@@ -65,7 +65,15 @@ export default function ResultsView({ jobId, onBack }) {
65
  ) : (
66
  <>
67
  CCR score = mean cosine similarity between each text and a construct&apos;s scale
68
- items. Higher = the text expresses the construct more strongly.
 
 
 
 
 
 
 
 
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&apos;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
  </>
packages/construct_library/apply_review.py CHANGED
@@ -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
- "team_psychological_safety_scale_4":
87
- "It is safe to take a risk in this team.",
 
 
 
 
 
 
 
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
- "team_psychological_safety_scale":
120
- "https://novopsych.com/wp-content/uploads/2025/08/TPS-7-questionnaire.pdf",
 
 
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. Only the DOI she supplied verbatim is
130
- # added; nothing is looked up or inferred.
 
 
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), 118-128. https://doi.org/10.1037/0022-3514.74.1.118",
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), 118-128. https://doi.org/10.1037/0022-3514.74.1.118",
144
  }
145
 
146
  QUESTIONNAIRE_FIX = {
@@ -148,31 +206,39 @@ QUESTIONNAIRE_FIX = {
148
  "individualism_horizontal": "Horizontal and Vertical Individualism and Collectivism Scale",
149
  }
150
 
151
- # Gaps the reviewer reported without a replacement: recorded, never guessed at.
 
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
- flags[item_id] = str(r[idx["REVERSE-SCORED? (Y/N)"]] or "").strip().upper() == "Y"
 
 
 
 
 
 
 
 
 
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" pending PI : {len(PENDING)} -> {sorted(PENDING)}")
 
 
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
packages/construct_library/constructs/bas_2_v2.yaml CHANGED
@@ -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: verified
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: verified
20
- notes: Reviewer could not reach the recorded source URL; wording checked against the publisher PDF.
21
- A working source link is still needed.
 
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.
packages/construct_library/constructs/cage_questionnaire_v2.yaml CHANGED
@@ -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: verified
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: verified
21
- notes: Citation is correct but the recorded source URL is no longer accessible; a working source link
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?
packages/construct_library/constructs/cbi_work_related_burnout.yaml CHANGED
@@ -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: verified
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: verified
20
- notes: Reviewer notes the source numbers these items 7 to 13.
 
 
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?
packages/construct_library/constructs/collectivism_horizontal.yaml CHANGED
@@ -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), 118-128. 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
  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
packages/construct_library/constructs/individualism_horizontal_v2.yaml CHANGED
@@ -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), 118-128. https://doi.org/10.1037/0022-3514.74.1.118
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
packages/construct_library/constructs/mfq_care.yaml CHANGED
@@ -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: 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
  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.
packages/construct_library/constructs/mfq_fairness.yaml CHANGED
@@ -1,5 +1,4 @@
1
- # Superseded by version 2 (spec 0007). Kept so runs that used this
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: archived
12
  review:
13
  reviewer: Noor Skhiri
14
  date: '2026-08-25'
15
- outcome: superseded_by_version_2
 
 
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.
packages/construct_library/constructs/mfq_fairness_v2.yaml DELETED
@@ -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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
packages/construct_library/constructs/rses_v2.yaml CHANGED
@@ -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: verified
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: verified
 
 
 
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.
packages/construct_library/constructs/team_psychological_safety_scale.yaml CHANGED
@@ -1,5 +1,4 @@
1
- # Superseded by version 2 (spec 0007). Kept so runs that used this
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: archived
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: not_provided_pending_verification
16
  imported_from: lib.xlsx
17
  review:
18
  reviewer: Noor Skhiri
19
  date: '2026-08-25'
20
- outcome: superseded_by_version_2
 
 
 
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.
packages/construct_library/constructs/team_psychological_safety_scale_v2.yaml DELETED
@@ -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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
packages/construct_library/validate_constructs.py CHANGED
@@ -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
- print(f" WARN {name}: ALL items reverse-scored - aggregate will be blocked under exclude_reversed.")
 
 
 
 
 
 
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']}]")