@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.
- package/CHANGELOG.md +10 -0
- package/README.md +7 -2
- package/dist/universal/.agents/skills/citable/SKILL.md +7 -2
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/commands/README.md +1 -0
- package/dist/universal/.agents/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.agents/skills/citable/manifest.json +9 -6
- package/dist/universal/.agents/skills/citable/schemas/review-item.schema.json +12 -0
- package/dist/universal/.agents/skills/citable/schemas/sampling-plan.schema.json +4 -0
- package/dist/universal/.claude/skills/citable/SKILL.md +7 -2
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/commands/README.md +1 -0
- package/dist/universal/.claude/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.claude/skills/citable/manifest.json +9 -6
- package/dist/universal/.claude/skills/citable/schemas/review-item.schema.json +12 -0
- package/dist/universal/.claude/skills/citable/schemas/sampling-plan.schema.json +4 -0
- package/dist/universal/.cursor/skills/citable/SKILL.md +7 -2
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/commands/README.md +1 -0
- package/dist/universal/.cursor/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.cursor/skills/citable/manifest.json +9 -6
- package/dist/universal/.cursor/skills/citable/schemas/review-item.schema.json +12 -0
- package/dist/universal/.cursor/skills/citable/schemas/sampling-plan.schema.json +4 -0
- package/dist/universal/.gemini/skills/citable/SKILL.md +7 -2
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/commands/README.md +1 -0
- package/dist/universal/.gemini/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.gemini/skills/citable/manifest.json +9 -6
- package/dist/universal/.gemini/skills/citable/schemas/review-item.schema.json +12 -0
- package/dist/universal/.gemini/skills/citable/schemas/sampling-plan.schema.json +4 -0
- package/dist/universal/.github/skills/citable/SKILL.md +7 -2
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/commands/README.md +1 -0
- package/dist/universal/.github/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.github/skills/citable/manifest.json +9 -6
- package/dist/universal/.github/skills/citable/schemas/review-item.schema.json +12 -0
- package/dist/universal/.github/skills/citable/schemas/sampling-plan.schema.json +4 -0
- package/dist/universal/.kiro/skills/citable/SKILL.md +7 -2
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/commands/README.md +1 -0
- package/dist/universal/.kiro/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.kiro/skills/citable/manifest.json +9 -6
- package/dist/universal/.kiro/skills/citable/schemas/review-item.schema.json +12 -0
- package/dist/universal/.kiro/skills/citable/schemas/sampling-plan.schema.json +4 -0
- package/dist/universal/.opencode/skills/citable/SKILL.md +7 -2
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/commands/README.md +1 -0
- package/dist/universal/.opencode/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.opencode/skills/citable/manifest.json +9 -6
- package/dist/universal/.opencode/skills/citable/schemas/review-item.schema.json +12 -0
- package/dist/universal/.opencode/skills/citable/schemas/sampling-plan.schema.json +4 -0
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +7 -2
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/commands/README.md +1 -0
- package/dist/universal/.pi/agent/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.pi/agent/skills/citable/manifest.json +9 -6
- package/dist/universal/.pi/agent/skills/citable/schemas/review-item.schema.json +12 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/sampling-plan.schema.json +4 -0
- package/dist/universal/.qoder/skills/citable/SKILL.md +7 -2
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/commands/README.md +1 -0
- package/dist/universal/.qoder/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.qoder/skills/citable/manifest.json +9 -6
- package/dist/universal/.qoder/skills/citable/schemas/review-item.schema.json +12 -0
- package/dist/universal/.qoder/skills/citable/schemas/sampling-plan.schema.json +4 -0
- package/dist/universal/.rovodev/skills/citable/SKILL.md +7 -2
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/commands/README.md +1 -0
- package/dist/universal/.rovodev/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.rovodev/skills/citable/manifest.json +9 -6
- package/dist/universal/.rovodev/skills/citable/schemas/review-item.schema.json +12 -0
- package/dist/universal/.rovodev/skills/citable/schemas/sampling-plan.schema.json +4 -0
- package/dist/universal/.trae/skills/citable/SKILL.md +7 -2
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/commands/README.md +1 -0
- package/dist/universal/.trae/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.trae/skills/citable/manifest.json +9 -6
- package/dist/universal/.trae/skills/citable/schemas/review-item.schema.json +12 -0
- package/dist/universal/.trae/skills/citable/schemas/sampling-plan.schema.json +4 -0
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +7 -2
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/commands/README.md +1 -0
- package/dist/universal/.trae-cn/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.trae-cn/skills/citable/manifest.json +9 -6
- package/dist/universal/.trae-cn/skills/citable/schemas/review-item.schema.json +12 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/sampling-plan.schema.json +4 -0
- package/dist/universal/manifest.json +111 -75
- package/package.json +1 -1
- package/schemas/review-item.schema.json +12 -0
- package/schemas/sampling-plan.schema.json +4 -0
- package/skill/SKILL.md +7 -2
- package/skill/commands/README.md +1 -0
- package/skill/commands/reviews.md +18 -0
- package/src/cli/index.js +17 -0
- package/src/commands/reviews.js +73 -0
- 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.
|
|
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
|
package/skill/commands/README.md
CHANGED
|
@@ -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
|
+
}
|
package/src/registries/index.js
CHANGED
|
@@ -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
|
|