@nebulacomponents/citable 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +7 -2
  3. package/dist/universal/.agents/skills/citable/SKILL.md +7 -2
  4. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  5. package/dist/universal/.agents/skills/citable/commands/README.md +1 -0
  6. package/dist/universal/.agents/skills/citable/commands/reviews.md +18 -0
  7. package/dist/universal/.agents/skills/citable/manifest.json +9 -6
  8. package/dist/universal/.agents/skills/citable/schemas/review-item.schema.json +12 -0
  9. package/dist/universal/.agents/skills/citable/schemas/sampling-plan.schema.json +4 -0
  10. package/dist/universal/.claude/skills/citable/SKILL.md +7 -2
  11. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  12. package/dist/universal/.claude/skills/citable/commands/README.md +1 -0
  13. package/dist/universal/.claude/skills/citable/commands/reviews.md +18 -0
  14. package/dist/universal/.claude/skills/citable/manifest.json +9 -6
  15. package/dist/universal/.claude/skills/citable/schemas/review-item.schema.json +12 -0
  16. package/dist/universal/.claude/skills/citable/schemas/sampling-plan.schema.json +4 -0
  17. package/dist/universal/.cursor/skills/citable/SKILL.md +7 -2
  18. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  19. package/dist/universal/.cursor/skills/citable/commands/README.md +1 -0
  20. package/dist/universal/.cursor/skills/citable/commands/reviews.md +18 -0
  21. package/dist/universal/.cursor/skills/citable/manifest.json +9 -6
  22. package/dist/universal/.cursor/skills/citable/schemas/review-item.schema.json +12 -0
  23. package/dist/universal/.cursor/skills/citable/schemas/sampling-plan.schema.json +4 -0
  24. package/dist/universal/.gemini/skills/citable/SKILL.md +7 -2
  25. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  26. package/dist/universal/.gemini/skills/citable/commands/README.md +1 -0
  27. package/dist/universal/.gemini/skills/citable/commands/reviews.md +18 -0
  28. package/dist/universal/.gemini/skills/citable/manifest.json +9 -6
  29. package/dist/universal/.gemini/skills/citable/schemas/review-item.schema.json +12 -0
  30. package/dist/universal/.gemini/skills/citable/schemas/sampling-plan.schema.json +4 -0
  31. package/dist/universal/.github/skills/citable/SKILL.md +7 -2
  32. package/dist/universal/.github/skills/citable/VERSION +1 -1
  33. package/dist/universal/.github/skills/citable/commands/README.md +1 -0
  34. package/dist/universal/.github/skills/citable/commands/reviews.md +18 -0
  35. package/dist/universal/.github/skills/citable/manifest.json +9 -6
  36. package/dist/universal/.github/skills/citable/schemas/review-item.schema.json +12 -0
  37. package/dist/universal/.github/skills/citable/schemas/sampling-plan.schema.json +4 -0
  38. package/dist/universal/.kiro/skills/citable/SKILL.md +7 -2
  39. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  40. package/dist/universal/.kiro/skills/citable/commands/README.md +1 -0
  41. package/dist/universal/.kiro/skills/citable/commands/reviews.md +18 -0
  42. package/dist/universal/.kiro/skills/citable/manifest.json +9 -6
  43. package/dist/universal/.kiro/skills/citable/schemas/review-item.schema.json +12 -0
  44. package/dist/universal/.kiro/skills/citable/schemas/sampling-plan.schema.json +4 -0
  45. package/dist/universal/.opencode/skills/citable/SKILL.md +7 -2
  46. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  47. package/dist/universal/.opencode/skills/citable/commands/README.md +1 -0
  48. package/dist/universal/.opencode/skills/citable/commands/reviews.md +18 -0
  49. package/dist/universal/.opencode/skills/citable/manifest.json +9 -6
  50. package/dist/universal/.opencode/skills/citable/schemas/review-item.schema.json +12 -0
  51. package/dist/universal/.opencode/skills/citable/schemas/sampling-plan.schema.json +4 -0
  52. package/dist/universal/.pi/agent/skills/citable/SKILL.md +7 -2
  53. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  54. package/dist/universal/.pi/agent/skills/citable/commands/README.md +1 -0
  55. package/dist/universal/.pi/agent/skills/citable/commands/reviews.md +18 -0
  56. package/dist/universal/.pi/agent/skills/citable/manifest.json +9 -6
  57. package/dist/universal/.pi/agent/skills/citable/schemas/review-item.schema.json +12 -0
  58. package/dist/universal/.pi/agent/skills/citable/schemas/sampling-plan.schema.json +4 -0
  59. package/dist/universal/.qoder/skills/citable/SKILL.md +7 -2
  60. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  61. package/dist/universal/.qoder/skills/citable/commands/README.md +1 -0
  62. package/dist/universal/.qoder/skills/citable/commands/reviews.md +18 -0
  63. package/dist/universal/.qoder/skills/citable/manifest.json +9 -6
  64. package/dist/universal/.qoder/skills/citable/schemas/review-item.schema.json +12 -0
  65. package/dist/universal/.qoder/skills/citable/schemas/sampling-plan.schema.json +4 -0
  66. package/dist/universal/.rovodev/skills/citable/SKILL.md +7 -2
  67. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  68. package/dist/universal/.rovodev/skills/citable/commands/README.md +1 -0
  69. package/dist/universal/.rovodev/skills/citable/commands/reviews.md +18 -0
  70. package/dist/universal/.rovodev/skills/citable/manifest.json +9 -6
  71. package/dist/universal/.rovodev/skills/citable/schemas/review-item.schema.json +12 -0
  72. package/dist/universal/.rovodev/skills/citable/schemas/sampling-plan.schema.json +4 -0
  73. package/dist/universal/.trae/skills/citable/SKILL.md +7 -2
  74. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  75. package/dist/universal/.trae/skills/citable/commands/README.md +1 -0
  76. package/dist/universal/.trae/skills/citable/commands/reviews.md +18 -0
  77. package/dist/universal/.trae/skills/citable/manifest.json +9 -6
  78. package/dist/universal/.trae/skills/citable/schemas/review-item.schema.json +12 -0
  79. package/dist/universal/.trae/skills/citable/schemas/sampling-plan.schema.json +4 -0
  80. package/dist/universal/.trae-cn/skills/citable/SKILL.md +7 -2
  81. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  82. package/dist/universal/.trae-cn/skills/citable/commands/README.md +1 -0
  83. package/dist/universal/.trae-cn/skills/citable/commands/reviews.md +18 -0
  84. package/dist/universal/.trae-cn/skills/citable/manifest.json +9 -6
  85. package/dist/universal/.trae-cn/skills/citable/schemas/review-item.schema.json +12 -0
  86. package/dist/universal/.trae-cn/skills/citable/schemas/sampling-plan.schema.json +4 -0
  87. package/dist/universal/manifest.json +111 -75
  88. package/package.json +1 -1
  89. package/schemas/review-item.schema.json +12 -0
  90. package/schemas/sampling-plan.schema.json +4 -0
  91. package/skill/SKILL.md +7 -2
  92. package/skill/commands/README.md +1 -0
  93. package/skill/commands/reviews.md +18 -0
  94. package/src/cli/index.js +17 -0
  95. package/src/commands/reviews.js +73 -0
  96. package/src/registries/index.js +9 -0
@@ -0,0 +1,12 @@
1
+ {
2
+ "$id":"citable://schemas/review-item.schema.json","$schema":"http://json-schema.org/draft-07/schema#","title":"Semantic review item registry","type":"object","additionalProperties":false,"required":["version","kind","updated","entries"],
3
+ "properties":{"version":{"const":1},"kind":{"const":"review_items"},"updated":{"type":"string","format":"date-time"},"entries":{"type":"array","items":{"type":"object","additionalProperties":false,
4
+ "required":["review_item_id","source_run_id","finding_id","finding_hash","review_type","status","priority_inputs","priority_score","priority_tier","missing_inputs","policy_id","assigned_reviewer_ids","decisions","created_at"],
5
+ "properties":{"review_item_id":{"type":"string","pattern":"^REVIEW-[A-Z0-9][A-Z0-9_-]*$"},"source_run_id":{"type":"string"},"finding_id":{"type":"string"},"finding_hash":{"type":"string","pattern":"^[a-f0-9]{64}$"},
6
+ "review_type":{"type":"string","enum":["claim_support","citation_correctness","passage_integrity","narrative_accuracy","recommendation_eligibility","comparison_fairness","intent_alignment","evidence_strength","general_semantic_review"]},
7
+ "status":{"type":"string","enum":["queued","assigned","in_review","adjudication_required","completed","withdrawn"]},
8
+ "priority_inputs":{"type":"object","additionalProperties":false,"required":["business_importance","citation_frequency","evidence_age_days","source_authority","regulatory_exposure","intervention_impact","change_magnitude","disagreement_history"],"properties":{"business_importance":{"type":["integer","null"],"minimum":0,"maximum":5},"citation_frequency":{"type":["integer","null"],"minimum":0},"evidence_age_days":{"type":["integer","null"],"minimum":0},"source_authority":{"type":["integer","null"],"minimum":0,"maximum":5},"regulatory_exposure":{"type":["integer","null"],"minimum":0,"maximum":5},"intervention_impact":{"type":["integer","null"],"minimum":0,"maximum":5},"change_magnitude":{"type":["integer","null"],"minimum":0,"maximum":5},"disagreement_history":{"type":["integer","null"],"minimum":0}}},
9
+ "priority_score":{"type":"number","minimum":0},"priority_tier":{"type":"string","enum":["urgent","high","normal","insufficient_inputs"]},"missing_inputs":{"type":"array","items":{"type":"string"}},"policy_id":{"type":"string","pattern":"^POLICY-"},"assigned_reviewer_ids":{"type":"array","uniqueItems":true,"items":{"type":"string","pattern":"^REVIEWER-"}},
10
+ "decisions":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["reviewer_id","verdict","rubric","evidence_refs","rationale","decided_at","review_item_hash"],"properties":{"reviewer_id":{"type":"string","pattern":"^REVIEWER-"},"verdict":{"type":"string","enum":["supported","partially_supported","unsupported","inconclusive","not_applicable"]},"rubric":{"type":"string"},"evidence_refs":{"type":"array","minItems":1,"items":{"type":"string"}},"rationale":{"type":"string","minLength":1},"decided_at":{"type":"string","format":"date-time"},"review_item_hash":{"type":"string","pattern":"^[a-f0-9]{64}$"}}}},"adjudication":{"type":["object","null"],"additionalProperties":false,"required":["reviewer_id","verdict","evidence_refs","rationale","decided_at","review_item_hash"],"properties":{"reviewer_id":{"type":"string","pattern":"^REVIEWER-"},"verdict":{"type":"string","enum":["supported","partially_supported","unsupported","inconclusive","not_applicable"]},"evidence_refs":{"type":"array","minItems":1,"items":{"type":"string"}},"rationale":{"type":"string","minLength":1},"decided_at":{"type":"string","format":"date-time"},"review_item_hash":{"type":"string","pattern":"^[a-f0-9]{64}$"}}},"created_at":{"type":"string","format":"date-time"}
11
+ }}}}
12
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "$id":"citable://schemas/sampling-plan.schema.json","$schema":"http://json-schema.org/draft-07/schema#","title":"Semantic review sampling plan registry","type":"object","additionalProperties":false,"required":["version","kind","updated","entries"],
3
+ "properties":{"version":{"const":1},"kind":{"const":"sampling_plans"},"updated":{"type":"string","format":"date-time"},"entries":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["sampling_plan_id","status","method","population_statuses","review_types","sample_size","seed","inclusion_criteria","exclusion_criteria","reviewer_assignment_method","extrapolation_limits","population_hash","selected_item_ids","created_at"],"properties":{"sampling_plan_id":{"type":"string","pattern":"^SAMPLE-[A-Z0-9][A-Z0-9_-]*$"},"status":{"type":"string","enum":["draft","selected","completed"]},"method":{"type":"string","enum":["census","simple_random"]},"population_statuses":{"type":"array","minItems":1,"items":{"type":"string","enum":["queued","assigned","in_review","adjudication_required","completed"]}},"review_types":{"type":"array","items":{"type":"string"}},"sample_size":{"type":"integer","minimum":1},"seed":{"type":"string","minLength":1},"inclusion_criteria":{"type":"array","items":{"type":"string"}},"exclusion_criteria":{"type":"array","items":{"type":"string"}},"reviewer_assignment_method":{"type":"string","minLength":1},"extrapolation_limits":{"type":"array","minItems":1,"items":{"type":"string"}},"population_hash":{"type":["string","null"],"pattern":"^[a-f0-9]{64}$"},"selected_item_ids":{"type":"array","uniqueItems":true,"items":{"type":"string","pattern":"^REVIEW-"}},"created_at":{"type":"string","format":"date-time"}}}}}
4
+ }
package/skill/SKILL.md CHANGED
@@ -7,7 +7,7 @@ description: >
7
7
  generative engine optimization, structured data governance, claim substantiation,
8
8
  crawler policy, entity consistency, content discoverability, or wants a site audited
9
9
  for how search and AI systems will retrieve, understand, cite, or recommend it.
10
- version: 1.8.0
10
+ version: 1.9.0
11
11
  ---
12
12
 
13
13
  # Citable — evidence and change control for search and AI citation readiness
@@ -110,6 +110,11 @@ citable objectives validate
110
110
  citable evaluate [objective-id] [--ref-date YYYY-MM-DD]
111
111
  citable governance validate [--ref-date YYYY-MM-DD]
112
112
  citable governance evaluate <run-id> [--ref-date YYYY-MM-DD]
113
+ citable reviews queue <run-id> <policy-id> [--write]
114
+ citable reviews prioritize [--write]
115
+ citable reviews plan --input <json|yaml> [--write]
116
+ citable reviews sample <sampling-plan-id> [--write]
117
+ citable reviews evaluate
113
118
  ```
114
119
 
115
120
  Audit scopes: `technical seo aeo geo architecture entity claims evidence schema
@@ -169,7 +174,7 @@ mandatory.
169
174
  `.citable/` holds YAML registries for queries, prompts, entities, claims,
170
175
  evidence, pages, crawlers, competitors, experiments, metrics, objectives,
171
176
  interventions, optional connection state, reviewers, review policies, and
172
- governed exceptions, all schema-validated
177
+ governed exceptions, semantic review items, and sampling plans, all schema-validated
173
178
  (`schemas/*.schema.json`) with referential integrity checks. Rules:
174
179
 
175
180
  - Never overwrite registry content without history — use the loader/saver in
@@ -28,6 +28,7 @@ intent, not executability, so statuses are strict:
28
28
  | /citable objectives init / validate | implemented — user-owned metric selection, cohorts, windows, and guardrails |
29
29
  | /citable evaluate | implemented — independent baseline/evaluation comparisons with inconclusive handling |
30
30
  | /citable governance validate / evaluate | implemented — validates reviewer and exception authority and emits immutable dispositions without changing failed findings |
31
+ | /citable reviews queue / prioritize / plan / sample / evaluate | implemented — materiality queues, reproducible sampling, stale-decision checks, and independent disagreement adjudication |
31
32
  | /citable ingest, map-site, map-queries, map-prompts, map-entities, map-evidence | specified (ingest.md) |
32
33
  | /citable optimize-page | specified (optimize-page.md) — requires source-to-render mapping, claim-preserving rewrites, build execution, rollback; none of that is proven yet |
33
34
  | /citable create-page, answer-block, architect, interlink, consolidate, metadata | specified (page-work.md) |
@@ -0,0 +1,18 @@
1
+ # Semantic review queues and sampling
2
+
3
+ Use `reviews queue` to create hash-bound review work from heuristic or
4
+ review-required findings. Newly queued items expose all missing materiality
5
+ inputs and remain `insufficient_inputs`; model confidence alone never controls
6
+ priority. After authorized operators supply business importance, citation
7
+ frequency, evidence age and authority, regulatory exposure, intervention
8
+ impact, change magnitude, and disagreement history, use `reviews prioritize`.
9
+
10
+ Sampling plans support only `census` and reproducible `simple_random` methods.
11
+ Every selection records its seed, population hash, inclusion/exclusion criteria,
12
+ assignment method, and extrapolation limits. Do not present sampled review as
13
+ complete validation or generalize beyond the recorded population.
14
+
15
+ `reviews evaluate` rejects stale or unauthorized decisions. Distinct verdicts
16
+ produce `adjudication_required`; vote counting never establishes semantic truth.
17
+ Completion requires an evidence-linked, hash-bound decision from an active
18
+ independent reviewer.
package/src/cli/index.js CHANGED
@@ -18,6 +18,7 @@ import { monitor } from '../commands/monitor.js';
18
18
  import { evaluateObjective, importMetrics, initializeObjective, validateObjectives } from '../commands/measurement.js';
19
19
  import { configureConnection, connectionStatus, discoverConnections, disconnectConnection, syncConnection, validateConnection } from '../commands/connect.js';
20
20
  import { evaluateDispositions, validateGovernance } from '../commands/governance.js';
21
+ import { evaluateReviews, initializeSamplingPlan, prioritizeReviews, queueReviews, selectSample } from '../commands/reviews.js';
21
22
 
22
23
  const HELP = `citable — SEO / AEO / GEO audit, remediation, validation, and governance
23
24
 
@@ -56,6 +57,11 @@ Commands
56
57
  evaluate [objective-id] Compare objective baseline and evaluation windows
57
58
  governance validate Validate reviewer, policy, and exception controls
58
59
  governance evaluate [run] Produce immutable enforcement dispositions without changing findings
60
+ reviews queue [run policy] Create semantic review work from heuristic findings
61
+ reviews prioritize Rank review work from explicit materiality inputs
62
+ reviews plan Validate/add a sampling plan from --input
63
+ reviews sample [plan] Select a reproducible census or seeded random sample
64
+ reviews evaluate Detect stale decisions and require disagreement adjudication
59
65
  self-upgrade Check for a newer version and upgrade the npx cache
60
66
 
61
67
  Options
@@ -255,6 +261,17 @@ export async function main(argv = process.argv.slice(2), options = {}) {
255
261
  } else throw new Error('usage: citable governance <validate|evaluate [run-id]> [--ref-date YYYY-MM-DD]');
256
262
  break;
257
263
  }
264
+ case 'reviews': {
265
+ const mode=args._[0]; let r;
266
+ if(mode==='queue') r=queueReviews(root,{runId:args._[1],policyId:args._[2],write:args.write});
267
+ else if(mode==='prioritize') r=prioritizeReviews(root,{write:args.write});
268
+ else if(mode==='plan') r=initializeSamplingPlan(root,{input:args.input,write:args.write});
269
+ else if(mode==='sample') r=selectSample(root,{planId:args._[1],write:args.write});
270
+ else if(mode==='evaluate') { r=evaluateReviews(root); if(!r.ok) process.exitCode=1; }
271
+ else throw new Error('usage: citable reviews <queue|prioritize|plan|sample|evaluate> [options]');
272
+ out(args,`reviews ${mode}: ${r.created?.length ?? r.items?.length ?? r.selected_item_ids?.length ?? r.results?.length ?? 1} item(s)${args.write?' written':' (dry run)'}`,r);
273
+ break;
274
+ }
258
275
  case 'self-upgrade': {
259
276
  const output = await selfUpgradeCommand(argv.slice(1));
260
277
  console.log(output);
@@ -0,0 +1,73 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { loadRegistries, saveRegistry } from '../registries/index.js';
4
+ import { readJson, readYaml, sha256, nowIso } from '../shared/io.js';
5
+ import { validateAgainst } from '../shared/schemaValidator.js';
6
+
7
+ const INPUTS = ['business_importance','citation_frequency','evidence_age_days','source_authority','regulatory_exposure','intervention_impact','change_magnitude','disagreement_history'];
8
+ const SEVERITY = { critical: 25, high: 20, medium: 12, low: 6, informational: 2, experimental: 1 };
9
+
10
+ export function reviewBindingHash(item) {
11
+ const { decisions, status, adjudication, ...bound } = item;
12
+ return sha256(JSON.stringify(bound));
13
+ }
14
+
15
+ function priority(item, finding) {
16
+ const missing = INPUTS.filter((key) => item.priority_inputs[key] == null);
17
+ const p = item.priority_inputs;
18
+ const score = (SEVERITY[finding?.classification?.severity] || 0) + (p.business_importance || 0) * 4 + Math.min(p.citation_frequency || 0, 10) + Math.min((p.evidence_age_days || 0) / 30, 10) + (5 - (p.source_authority ?? 5)) * 2 + (p.regulatory_exposure || 0) * 5 + (p.intervention_impact || 0) * 3 + (p.change_magnitude || 0) * 2 + Math.min((p.disagreement_history || 0) * 3, 12);
19
+ return { score, tier: missing.length ? 'insufficient_inputs' : score >= 60 ? 'urgent' : score >= 35 ? 'high' : 'normal', missing };
20
+ }
21
+
22
+ export function queueReviews(root, { runId, policyId, write = false }) {
23
+ if (!runId || !policyId) throw new Error('reviews queue requires <run-id> <policy-id>');
24
+ const file = path.join(root,'.citable','runs',runId,'findings.json');
25
+ if (!fs.existsSync(file)) throw new Error(`source findings not found for run ${runId}`);
26
+ const findings = readJson(file);
27
+ const { registries, problems } = loadRegistries(root);
28
+ if (problems.length) throw new Error(`registry validation failed: ${problems.join('; ')}`);
29
+ if (!registries.review_policies.entries.some((p) => p.policy_id === policyId && p.status === 'active')) throw new Error(`active review policy not found: ${policyId}`);
30
+ const existing = new Set(registries.review_items.entries.map((i) => `${i.source_run_id}:${i.finding_id}`));
31
+ const candidates = findings.filter((f) => !f.classification.deterministic || f.remediation.review_required).filter((f) => !existing.has(`${runId}:${f.finding_id}`));
32
+ const created = candidates.map((finding) => {
33
+ const item = { review_item_id:`REVIEW-${sha256(`${runId}:${finding.finding_id}`).slice(0,12).toUpperCase()}`,source_run_id:runId,finding_id:finding.finding_id,finding_hash:sha256(JSON.stringify(finding)),review_type:'general_semantic_review',status:'queued',priority_inputs:Object.fromEntries(INPUTS.map((k)=>[k,null])),priority_score:0,priority_tier:'insufficient_inputs',missing_inputs:[...INPUTS],policy_id:policyId,assigned_reviewer_ids:[],decisions:[],adjudication:null,created_at:nowIso() };
34
+ const ranked = priority(item,finding); item.priority_score=ranked.score; item.priority_tier=ranked.tier; item.missing_inputs=ranked.missing; return item;
35
+ });
36
+ if (write && created.length) saveRegistry(root,'review_items',{...registries.review_items,entries:[...registries.review_items.entries,...created]});
37
+ return { source_run_id:runId, created, written:write };
38
+ }
39
+
40
+ export function prioritizeReviews(root, { write = false } = {}) {
41
+ const { registries, problems } = loadRegistries(root); if (problems.length) throw new Error(`registry validation failed: ${problems.join('; ')}`);
42
+ const cache = new Map();
43
+ const entries = registries.review_items.entries.map((item) => { const file=path.join(root,'.citable','runs',item.source_run_id,'findings.json'); if(!cache.has(file)) cache.set(file,fs.existsSync(file)?new Map(readJson(file).map(f=>[f.finding_id,f])):new Map()); const ranked=priority(item,cache.get(file).get(item.finding_id)); return {...item,priority_score:ranked.score,priority_tier:ranked.tier,missing_inputs:ranked.missing}; });
44
+ if(write) saveRegistry(root,'review_items',{...registries.review_items,entries});
45
+ return { items:entries.sort((a,b)=>b.priority_score-a.priority_score), written:write };
46
+ }
47
+
48
+ export function initializeSamplingPlan(root,{input,write=false}) {
49
+ if(!input || !fs.existsSync(input)) throw new Error('reviews plan requires --input <json|yaml>');
50
+ const plan=path.extname(input).match(/ya?ml/i)?readYaml(input):readJson(input); const {registries}=loadRegistries(root);
51
+ const candidate={...registries.sampling_plans,entries:[...registries.sampling_plans.entries,plan]};
52
+ const check=validateAgainst('sampling-plan.schema.json',candidate); if(!check.valid) throw new Error(`sampling plan violates contract: ${check.errors.join('; ')}`);
53
+ if(write) saveRegistry(root,'sampling_plans',candidate);
54
+ return { plan, written:write };
55
+ }
56
+
57
+ export function selectSample(root,{planId,write=false}) {
58
+ const {registries,problems}=loadRegistries(root); if(problems.length) throw new Error(`registry validation failed: ${problems.join('; ')}`);
59
+ const plan=registries.sampling_plans.entries.find(p=>p.sampling_plan_id===planId); if(!plan) throw new Error(`sampling plan not found: ${planId}`);
60
+ const population=registries.review_items.entries.filter(i=>plan.population_statuses.includes(i.status) && (!plan.review_types.length||plan.review_types.includes(i.review_type))).sort((a,b)=>a.review_item_id.localeCompare(b.review_item_id));
61
+ if(!population.length) throw new Error('sampling population is empty');
62
+ if(plan.method==='simple_random' && plan.sample_size>population.length) throw new Error('sample_size exceeds population');
63
+ const selected=plan.method==='census'?population:[...population].sort((a,b)=>sha256(`${plan.seed}:${a.review_item_id}`).localeCompare(sha256(`${plan.seed}:${b.review_item_id}`))).slice(0,plan.sample_size);
64
+ const updated={...plan,status:'selected',population_hash:sha256(JSON.stringify(population.map(i=>[i.review_item_id,reviewBindingHash(i)]))),selected_item_ids:selected.map(i=>i.review_item_id)};
65
+ if(write) saveRegistry(root,'sampling_plans',{...registries.sampling_plans,entries:registries.sampling_plans.entries.map(p=>p.sampling_plan_id===planId?updated:p)});
66
+ return { plan:updated,population_size:population.length,selected_item_ids:updated.selected_item_ids,written:write };
67
+ }
68
+
69
+ export function evaluateReviews(root) {
70
+ const {registries,problems}=loadRegistries(root); const reviewers=new Map(registries.reviewers.entries.map(r=>[r.reviewer_id,r])); const results=[];
71
+ for(const item of registries.review_items.entries){const issues=[];const binding=reviewBindingHash(item);const source=path.join(root,'.citable','runs',item.source_run_id,'findings.json');const finding=fs.existsSync(source)?readJson(source).find(f=>f.finding_id===item.finding_id):null;if(!finding||sha256(JSON.stringify(finding))!==item.finding_hash)issues.push('source finding is unavailable or changed');for(const d of item.decisions){const r=reviewers.get(d.reviewer_id);const reviewRole=r?.roles.some(role=>['technical_reviewer','subject_matter_reviewer','independent_reviewer'].includes(role));if(!r||r.status!=='active'||!reviewRole||!item.assigned_reviewer_ids.includes(d.reviewer_id))issues.push(`${d.reviewer_id} is not an active assigned semantic reviewer`);if(d.review_item_hash!==binding)issues.push(`${d.reviewer_id} decision is stale`);}const verdicts=[...new Set(item.decisions.map(d=>d.verdict))];let state=item.decisions.length?'completed':'review_required';if(verdicts.length>1){state='adjudication_required';const a=reviewers.get(item.adjudication?.reviewer_id);if(item.adjudication&&a?.status==='active'&&a.roles.includes('independent_reviewer')&&item.adjudication.review_item_hash===binding)state='completed';else issues.push('reviewer disagreement requires a current decision from an active independent adjudicator');}results.push({review_item_id:item.review_item_id,state,verdict:state==='completed'&&!issues.length?(item.adjudication?.verdict||verdicts[0]):null,issues});}
72
+ return {ok:problems.length===0&&results.every(r=>!r.issues.length),problems,results};
73
+ }
@@ -20,6 +20,8 @@ export const REGISTRY_SPECS = [
20
20
  { file: 'reviewers.yaml', kind: 'reviewers', schema: 'reviewer.schema.json', idField: 'reviewer_id' },
21
21
  { file: 'review-policies.yaml', kind: 'review_policies', schema: 'review-policy.schema.json', idField: 'policy_id' },
22
22
  { file: 'exceptions.yaml', kind: 'exceptions', schema: 'exception.schema.json', idField: 'exception_id' },
23
+ { file: 'review-items.yaml', kind: 'review_items', schema: 'review-item.schema.json', idField: 'review_item_id' },
24
+ { file: 'sampling-plans.yaml', kind: 'sampling_plans', schema: 'sampling-plan.schema.json', idField: 'sampling_plan_id' },
23
25
  ];
24
26
 
25
27
  export function contextDir(root) {
@@ -121,6 +123,13 @@ export function checkReferentialIntegrity(registries) {
121
123
  if (exception.related_intervention_id && !interventionIds.has(exception.related_intervention_id)) problems.push(`${prefix}: references unknown intervention id "${exception.related_intervention_id}"`);
122
124
  for (const ref of [exception.supersedes_exception_id, exception.superseded_by_exception_id]) if (ref && !exceptionIds.has(ref)) problems.push(`${prefix}: references unknown exception id "${ref}"`);
123
125
  }
126
+ for (const item of registries.review_items?.entries || []) {
127
+ const prefix = `review_items/${item.review_item_id}`;
128
+ if (!policyIds.has(item.policy_id)) problems.push(`${prefix}: references unknown review policy id "${item.policy_id}"`);
129
+ for (const ref of [...(item.assigned_reviewer_ids || []), ...(item.decisions || []).map((decision) => decision.reviewer_id), item.adjudication?.reviewer_id].filter(Boolean)) if (!reviewerIds.has(ref)) problems.push(`${prefix}: references unknown reviewer id "${ref}"`);
130
+ }
131
+ const reviewItemIds = ids.review_items || new Set();
132
+ for (const plan of registries.sampling_plans?.entries || []) for (const ref of plan.selected_item_ids || []) if (!reviewItemIds.has(ref)) problems.push(`sampling_plans/${plan.sampling_plan_id}: references unknown review item id "${ref}"`);
124
133
  return problems;
125
134
  }
126
135