@nebulacomponents/citable 1.8.0 → 1.10.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 +20 -0
- package/README.md +9 -2
- package/dist/universal/.agents/skills/citable/SKILL.md +10 -2
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/commands/README.md +2 -0
- package/dist/universal/.agents/skills/citable/commands/delivery.md +18 -0
- package/dist/universal/.agents/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.agents/skills/citable/manifest.json +11 -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/.agents/skills/citable/schemas/schedule.schema.json +4 -0
- package/dist/universal/.claude/skills/citable/SKILL.md +10 -2
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/commands/README.md +2 -0
- package/dist/universal/.claude/skills/citable/commands/delivery.md +18 -0
- package/dist/universal/.claude/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.claude/skills/citable/manifest.json +11 -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/.claude/skills/citable/schemas/schedule.schema.json +4 -0
- package/dist/universal/.cursor/skills/citable/SKILL.md +10 -2
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/commands/README.md +2 -0
- package/dist/universal/.cursor/skills/citable/commands/delivery.md +18 -0
- package/dist/universal/.cursor/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.cursor/skills/citable/manifest.json +11 -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/.cursor/skills/citable/schemas/schedule.schema.json +4 -0
- package/dist/universal/.gemini/skills/citable/SKILL.md +10 -2
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/commands/README.md +2 -0
- package/dist/universal/.gemini/skills/citable/commands/delivery.md +18 -0
- package/dist/universal/.gemini/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.gemini/skills/citable/manifest.json +11 -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/.gemini/skills/citable/schemas/schedule.schema.json +4 -0
- package/dist/universal/.github/skills/citable/SKILL.md +10 -2
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/commands/README.md +2 -0
- package/dist/universal/.github/skills/citable/commands/delivery.md +18 -0
- package/dist/universal/.github/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.github/skills/citable/manifest.json +11 -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/.github/skills/citable/schemas/schedule.schema.json +4 -0
- package/dist/universal/.kiro/skills/citable/SKILL.md +10 -2
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/commands/README.md +2 -0
- package/dist/universal/.kiro/skills/citable/commands/delivery.md +18 -0
- package/dist/universal/.kiro/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.kiro/skills/citable/manifest.json +11 -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/.kiro/skills/citable/schemas/schedule.schema.json +4 -0
- package/dist/universal/.opencode/skills/citable/SKILL.md +10 -2
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/commands/README.md +2 -0
- package/dist/universal/.opencode/skills/citable/commands/delivery.md +18 -0
- package/dist/universal/.opencode/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.opencode/skills/citable/manifest.json +11 -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/.opencode/skills/citable/schemas/schedule.schema.json +4 -0
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +10 -2
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/commands/README.md +2 -0
- package/dist/universal/.pi/agent/skills/citable/commands/delivery.md +18 -0
- package/dist/universal/.pi/agent/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.pi/agent/skills/citable/manifest.json +11 -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/.pi/agent/skills/citable/schemas/schedule.schema.json +4 -0
- package/dist/universal/.qoder/skills/citable/SKILL.md +10 -2
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/commands/README.md +2 -0
- package/dist/universal/.qoder/skills/citable/commands/delivery.md +18 -0
- package/dist/universal/.qoder/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.qoder/skills/citable/manifest.json +11 -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/.qoder/skills/citable/schemas/schedule.schema.json +4 -0
- package/dist/universal/.rovodev/skills/citable/SKILL.md +10 -2
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/commands/README.md +2 -0
- package/dist/universal/.rovodev/skills/citable/commands/delivery.md +18 -0
- package/dist/universal/.rovodev/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.rovodev/skills/citable/manifest.json +11 -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/.rovodev/skills/citable/schemas/schedule.schema.json +4 -0
- package/dist/universal/.trae/skills/citable/SKILL.md +10 -2
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/commands/README.md +2 -0
- package/dist/universal/.trae/skills/citable/commands/delivery.md +18 -0
- package/dist/universal/.trae/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.trae/skills/citable/manifest.json +11 -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/skills/citable/schemas/schedule.schema.json +4 -0
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +10 -2
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/commands/README.md +2 -0
- package/dist/universal/.trae-cn/skills/citable/commands/delivery.md +18 -0
- package/dist/universal/.trae-cn/skills/citable/commands/reviews.md +18 -0
- package/dist/universal/.trae-cn/skills/citable/manifest.json +11 -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/.trae-cn/skills/citable/schemas/schedule.schema.json +4 -0
- package/dist/universal/manifest.json +135 -75
- package/package.json +1 -1
- package/schemas/review-item.schema.json +12 -0
- package/schemas/sampling-plan.schema.json +4 -0
- package/schemas/schedule.schema.json +4 -0
- package/skill/SKILL.md +10 -2
- package/skill/commands/README.md +2 -0
- package/skill/commands/delivery.md +18 -0
- package/skill/commands/reviews.md +18 -0
- package/src/cli/index.js +32 -0
- package/src/commands/compareSnapshots.js +15 -0
- package/src/commands/delivery.js +50 -0
- package/src/commands/reviews.js +73 -0
- package/src/registries/index.js +10 -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
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id":"citable://schemas/schedule.schema.json","$schema":"http://json-schema.org/draft-07/schema#","title":"Audit schedule registry","type":"object","additionalProperties":false,"required":["version","kind","updated","entries"],
|
|
3
|
+
"properties":{"version":{"const":1},"kind":{"const":"schedules"},"updated":{"type":"string","format":"date-time"},"entries":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["schedule_id","name","status","cron","owner","expected_tool_version","audit","retention_days","limitations"],"properties":{"schedule_id":{"type":"string","pattern":"^SCHEDULE-[A-Z0-9][A-Z0-9_-]*$"},"name":{"type":"string","minLength":1},"status":{"type":"string","enum":["active","paused","retired"]},"cron":{"type":"string","minLength":5},"owner":{"type":"string","minLength":1},"expected_tool_version":{"type":"string","pattern":"^\\d+\\.\\d+\\.\\d+$"},"audit":{"type":"object","additionalProperties":false,"required":["target","scope"],"properties":{"target":{"type":"string","minLength":1},"scope":{"type":["string","null"]},"base_url":{"type":["string","null"]}}},"retention_days":{"type":"integer","minimum":1},"limitations":{"type":"array","minItems":1,"items":{"type":"string"}}}}}}
|
|
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.10.0
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Citable — evidence and change control for search and AI citation readiness
|
|
@@ -110,6 +110,13 @@ 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
|
|
118
|
+
citable schedules run <schedule-id> [--ref-date YYYY-MM-DD]
|
|
119
|
+
citable project github <run-id>
|
|
113
120
|
```
|
|
114
121
|
|
|
115
122
|
Audit scopes: `technical seo aeo geo architecture entity claims evidence schema
|
|
@@ -169,7 +176,8 @@ mandatory.
|
|
|
169
176
|
`.citable/` holds YAML registries for queries, prompts, entities, claims,
|
|
170
177
|
evidence, pages, crawlers, competitors, experiments, metrics, objectives,
|
|
171
178
|
interventions, optional connection state, reviewers, review policies, and
|
|
172
|
-
governed exceptions,
|
|
179
|
+
governed exceptions, semantic review items, sampling plans, and audit schedules,
|
|
180
|
+
all schema-validated
|
|
173
181
|
(`schemas/*.schema.json`) with referential integrity checks. Rules:
|
|
174
182
|
|
|
175
183
|
- Never overwrite registry content without history — use the loader/saver in
|
package/skill/commands/README.md
CHANGED
|
@@ -28,6 +28,8 @@ 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 |
|
|
32
|
+
| /citable schedules run / project github | implemented — canonical scheduled audits and hash-bound non-authoritative GitHub projections |
|
|
31
33
|
| /citable ingest, map-site, map-queries, map-prompts, map-entities, map-evidence | specified (ingest.md) |
|
|
32
34
|
| /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
35
|
| /citable create-page, answer-block, architect, interlink, consolidate, metadata | specified (page-work.md) |
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Scheduling, projections, and differential delivery
|
|
2
|
+
|
|
3
|
+
`citable schedules run <schedule-id>` executes the same canonical audit function
|
|
4
|
+
used by direct CLI calls. Schedules are version-pinned and fail closed when
|
|
5
|
+
paused, missing, invalid, or bound to another Citable version. The external
|
|
6
|
+
cron service remains responsible for triggering the command; a declared cron
|
|
7
|
+
does not prove execution. Schedule metadata is stored outside the finalized
|
|
8
|
+
audit package and binds back to its manifest checksum.
|
|
9
|
+
|
|
10
|
+
`citable project github <run-id>` creates GitHub annotation JSON as a
|
|
11
|
+
non-authoritative projection. It records hashes of the source manifest and
|
|
12
|
+
findings and never edits the run. Dashboards and check annotations must link to
|
|
13
|
+
these artifacts rather than maintain separate finding state.
|
|
14
|
+
|
|
15
|
+
`compare-snapshots` reports observed comparability dimensions: resource input,
|
|
16
|
+
evidence artifact, detector set, configuration, observation method, tool
|
|
17
|
+
version, and possible external-system change. These dimensions identify why
|
|
18
|
+
runs may not be directly comparable; they do not establish causal attribution.
|
|
@@ -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,8 @@ 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';
|
|
22
|
+
import { projectGithub, runSchedule } from '../commands/delivery.js';
|
|
21
23
|
|
|
22
24
|
const HELP = `citable — SEO / AEO / GEO audit, remediation, validation, and governance
|
|
23
25
|
|
|
@@ -56,6 +58,13 @@ Commands
|
|
|
56
58
|
evaluate [objective-id] Compare objective baseline and evaluation windows
|
|
57
59
|
governance validate Validate reviewer, policy, and exception controls
|
|
58
60
|
governance evaluate [run] Produce immutable enforcement dispositions without changing findings
|
|
61
|
+
reviews queue [run policy] Create semantic review work from heuristic findings
|
|
62
|
+
reviews prioritize Rank review work from explicit materiality inputs
|
|
63
|
+
reviews plan Validate/add a sampling plan from --input
|
|
64
|
+
reviews sample [plan] Select a reproducible census or seeded random sample
|
|
65
|
+
reviews evaluate Detect stale decisions and require disagreement adjudication
|
|
66
|
+
schedules run [id] Execute an active version-pinned audit schedule
|
|
67
|
+
project github [run] Render non-authoritative GitHub annotations from a run
|
|
59
68
|
self-upgrade Check for a newer version and upgrade the npx cache
|
|
60
69
|
|
|
61
70
|
Options
|
|
@@ -255,6 +264,29 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
|
|
255
264
|
} else throw new Error('usage: citable governance <validate|evaluate [run-id]> [--ref-date YYYY-MM-DD]');
|
|
256
265
|
break;
|
|
257
266
|
}
|
|
267
|
+
case 'reviews': {
|
|
268
|
+
const mode=args._[0]; let r;
|
|
269
|
+
if(mode==='queue') r=queueReviews(root,{runId:args._[1],policyId:args._[2],write:args.write});
|
|
270
|
+
else if(mode==='prioritize') r=prioritizeReviews(root,{write:args.write});
|
|
271
|
+
else if(mode==='plan') r=initializeSamplingPlan(root,{input:args.input,write:args.write});
|
|
272
|
+
else if(mode==='sample') r=selectSample(root,{planId:args._[1],write:args.write});
|
|
273
|
+
else if(mode==='evaluate') { r=evaluateReviews(root); if(!r.ok) process.exitCode=1; }
|
|
274
|
+
else throw new Error('usage: citable reviews <queue|prioritize|plan|sample|evaluate> [options]');
|
|
275
|
+
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);
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case 'schedules': {
|
|
279
|
+
if(args._[0]!=='run') throw new Error('usage: citable schedules run <schedule-id> [--ref-date YYYY-MM-DD]');
|
|
280
|
+
const r=await runSchedule(root,{scheduleId:args._[1],refDate:args.refDate});
|
|
281
|
+
out(args,`schedule ${r.schedule_execution.schedule_id}: audit ${r.runId}\nEvidence package: ${r.dir}\nExecution record: ${r.execution_file}`,r);
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case 'project': {
|
|
285
|
+
if(args._[0]!=='github') throw new Error('usage: citable project github <run-id>');
|
|
286
|
+
const r=projectGithub(root,{runId:args._[1]});
|
|
287
|
+
out(args,`project github ${r.source_run_id}: ${r.annotations.length} annotation(s)\nProjection: ${path.join(r.dir,'annotations.json')}`,r);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
258
290
|
case 'self-upgrade': {
|
|
259
291
|
const output = await selfUpgradeCommand(argv.slice(1));
|
|
260
292
|
console.log(output);
|
|
@@ -27,6 +27,16 @@ export function compareSnapshots(root, { runA, runB } = {}) {
|
|
|
27
27
|
};
|
|
28
28
|
const a = load(runA);
|
|
29
29
|
const b = load(runB);
|
|
30
|
+
const changedKeys = (left = {}, right = {}) => [...new Set([...Object.keys(left), ...Object.keys(right)])].filter((key) => left[key] !== right[key]);
|
|
31
|
+
const changeDimensions = {
|
|
32
|
+
resource_changed: changedKeys(a.manifest.input_hashes, b.manifest.input_hashes),
|
|
33
|
+
evidence_artifacts_changed: changedKeys(a.manifest.output_hashes, b.manifest.output_hashes),
|
|
34
|
+
detector_set_changed: JSON.stringify(a.manifest.detectors_run) !== JSON.stringify(b.manifest.detectors_run),
|
|
35
|
+
configuration_changed: a.manifest.configuration_hash !== b.manifest.configuration_hash,
|
|
36
|
+
observation_method_changed: a.manifest.command !== b.manifest.command || JSON.stringify(a.manifest.argv) !== JSON.stringify(b.manifest.argv) || a.manifest.target?.kind !== b.manifest.target?.kind,
|
|
37
|
+
tool_changed: a.manifest.tool_version !== b.manifest.tool_version,
|
|
38
|
+
external_system_may_have_changed: a.manifest.target?.kind === 'url' || b.manifest.target?.kind === 'url',
|
|
39
|
+
};
|
|
30
40
|
const key = (f) => `${f.detector_id}|${f.subject.identifier}|${f.observation.summary}`;
|
|
31
41
|
const aKeys = new Map(a.findings.map((f) => [key(f), f]));
|
|
32
42
|
const bKeys = new Map(b.findings.map((f) => [key(f), f]));
|
|
@@ -38,6 +48,11 @@ export function compareSnapshots(root, { runA, runB } = {}) {
|
|
|
38
48
|
baseline_timestamp: a.manifest.timestamp,
|
|
39
49
|
comparison_timestamp: b.manifest.timestamp,
|
|
40
50
|
regressions, resolved, persisting,
|
|
51
|
+
comparability: {
|
|
52
|
+
comparable: !changeDimensions.detector_set_changed && !changeDimensions.configuration_changed && !changeDimensions.observation_method_changed && !changeDimensions.tool_changed,
|
|
53
|
+
change_dimensions: changeDimensions,
|
|
54
|
+
limitation: 'These dimensions identify observed differences between run envelopes; they do not establish what caused a finding change.',
|
|
55
|
+
},
|
|
41
56
|
summary: {
|
|
42
57
|
new_findings: regressions.length,
|
|
43
58
|
resolved_findings: resolved.length,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { audit } from './audit.js';
|
|
5
|
+
import { loadRegistries } from '../registries/index.js';
|
|
6
|
+
import { readJson, sha256, writeJson, nowIso } from '../shared/io.js';
|
|
7
|
+
|
|
8
|
+
const PKG = readJson(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../package.json'));
|
|
9
|
+
|
|
10
|
+
export async function runSchedule(root, { scheduleId, refDate } = {}) {
|
|
11
|
+
const { registries, problems } = loadRegistries(root);
|
|
12
|
+
if (problems.length) throw new Error(`registry validation failed: ${problems.join('; ')}`);
|
|
13
|
+
const schedule = registries.schedules.entries.find((item) => item.schedule_id === scheduleId);
|
|
14
|
+
if (!schedule) throw new Error(`schedule not found: ${scheduleId}`);
|
|
15
|
+
if (schedule.status !== 'active') throw new Error(`schedule ${scheduleId} is ${schedule.status}`);
|
|
16
|
+
if (schedule.expected_tool_version !== PKG.version) throw new Error(`schedule ${scheduleId} expects Citable ${schedule.expected_tool_version}, current version is ${PKG.version}`);
|
|
17
|
+
const result = await audit(root, { target: schedule.audit.target, scope: schedule.audit.scope, baseUrl: schedule.audit.base_url, refDate });
|
|
18
|
+
const execution = { schedule_id: scheduleId, cron: schedule.cron, owner: schedule.owner, executed_at: nowIso(), run_id: result.runId, manifest_hash: sha256(fs.readFileSync(path.join(result.dir, 'manifest.json'))), limitations: schedule.limitations };
|
|
19
|
+
const executionFile = path.join(root, '.citable', 'schedule-executions', `${result.runId}.json`);
|
|
20
|
+
writeJson(executionFile, execution);
|
|
21
|
+
return { ...result, schedule_execution: execution, execution_file: executionFile };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function annotationLevel(severity) {
|
|
25
|
+
if (['critical', 'high'].includes(severity)) return 'failure';
|
|
26
|
+
if (severity === 'medium') return 'warning';
|
|
27
|
+
return 'notice';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function projectGithub(root, { runId } = {}) {
|
|
31
|
+
if (!runId) throw new Error('projection requires a source run id');
|
|
32
|
+
const runDir = path.join(root, '.citable', 'runs', runId);
|
|
33
|
+
const findingsFile = path.join(runDir, 'findings.json');
|
|
34
|
+
const manifestFile = path.join(runDir, 'manifest.json');
|
|
35
|
+
if (!fs.existsSync(findingsFile) || !fs.existsSync(manifestFile)) throw new Error(`run ${runId} is missing findings or manifest evidence`);
|
|
36
|
+
const findingsBytes = fs.readFileSync(findingsFile);
|
|
37
|
+
const manifestBytes = fs.readFileSync(manifestFile);
|
|
38
|
+
const annotations = JSON.parse(findingsBytes).map((finding) => ({
|
|
39
|
+
level: annotationLevel(finding.classification.severity),
|
|
40
|
+
title: `${finding.detector_id}: ${finding.classification.severity}`,
|
|
41
|
+
message: finding.observation.summary,
|
|
42
|
+
path: finding.subject.source_file || null,
|
|
43
|
+
line: finding.subject.source_location ? Number(String(finding.subject.source_location).match(/\d+/)?.[0]) || null : null,
|
|
44
|
+
finding_id: finding.finding_id,
|
|
45
|
+
}));
|
|
46
|
+
const projection = { source_run_id: runId, source_manifest_hash: sha256(manifestBytes), source_findings_hash: sha256(findingsBytes), generated_at: nowIso(), projection: 'github-check-annotations', authoritative: false, annotations };
|
|
47
|
+
const dir = path.join(root, '.citable', 'projections', 'github', runId);
|
|
48
|
+
writeJson(path.join(dir, 'annotations.json'), projection);
|
|
49
|
+
return { ...projection, dir };
|
|
50
|
+
}
|
|
@@ -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,9 @@ 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' },
|
|
25
|
+
{ file: 'schedules.yaml', kind: 'schedules', schema: 'schedule.schema.json', idField: 'schedule_id' },
|
|
23
26
|
];
|
|
24
27
|
|
|
25
28
|
export function contextDir(root) {
|
|
@@ -121,6 +124,13 @@ export function checkReferentialIntegrity(registries) {
|
|
|
121
124
|
if (exception.related_intervention_id && !interventionIds.has(exception.related_intervention_id)) problems.push(`${prefix}: references unknown intervention id "${exception.related_intervention_id}"`);
|
|
122
125
|
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
126
|
}
|
|
127
|
+
for (const item of registries.review_items?.entries || []) {
|
|
128
|
+
const prefix = `review_items/${item.review_item_id}`;
|
|
129
|
+
if (!policyIds.has(item.policy_id)) problems.push(`${prefix}: references unknown review policy id "${item.policy_id}"`);
|
|
130
|
+
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}"`);
|
|
131
|
+
}
|
|
132
|
+
const reviewItemIds = ids.review_items || new Set();
|
|
133
|
+
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
134
|
return problems;
|
|
125
135
|
}
|
|
126
136
|
|