@nebulacomponents/citable 1.6.0 → 1.8.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 +36 -0
- package/README.md +19 -8
- package/dist/universal/.agents/skills/citable/SKILL.md +20 -4
- 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/governance.md +17 -0
- package/dist/universal/.agents/skills/citable/commands/measurement.md +15 -0
- package/dist/universal/.agents/skills/citable/manifest.json +17 -9
- package/dist/universal/.agents/skills/citable/references/capability-boundaries.md +5 -0
- package/dist/universal/.agents/skills/citable/references/evidence-authority.md +43 -0
- package/dist/universal/.agents/skills/citable/schemas/bing-webmaster.schema.json +25 -0
- package/dist/universal/.agents/skills/citable/schemas/crawler-identity.schema.json +42 -0
- package/dist/universal/.agents/skills/citable/schemas/evidence-authority.schema.json +15 -0
- package/dist/universal/.agents/skills/citable/schemas/exception.schema.json +70 -0
- package/dist/universal/.agents/skills/citable/schemas/observation.schema.json +3 -2
- package/dist/universal/.agents/skills/citable/schemas/review-policy.schema.json +35 -0
- package/dist/universal/.agents/skills/citable/schemas/reviewer.schema.json +32 -0
- package/dist/universal/.claude/skills/citable/SKILL.md +20 -4
- 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/governance.md +17 -0
- package/dist/universal/.claude/skills/citable/commands/measurement.md +15 -0
- package/dist/universal/.claude/skills/citable/manifest.json +17 -9
- package/dist/universal/.claude/skills/citable/references/capability-boundaries.md +5 -0
- package/dist/universal/.claude/skills/citable/references/evidence-authority.md +43 -0
- package/dist/universal/.claude/skills/citable/schemas/bing-webmaster.schema.json +25 -0
- package/dist/universal/.claude/skills/citable/schemas/crawler-identity.schema.json +42 -0
- package/dist/universal/.claude/skills/citable/schemas/evidence-authority.schema.json +15 -0
- package/dist/universal/.claude/skills/citable/schemas/exception.schema.json +70 -0
- package/dist/universal/.claude/skills/citable/schemas/observation.schema.json +3 -2
- package/dist/universal/.claude/skills/citable/schemas/review-policy.schema.json +35 -0
- package/dist/universal/.claude/skills/citable/schemas/reviewer.schema.json +32 -0
- package/dist/universal/.cursor/skills/citable/SKILL.md +20 -4
- 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/governance.md +17 -0
- package/dist/universal/.cursor/skills/citable/commands/measurement.md +15 -0
- package/dist/universal/.cursor/skills/citable/manifest.json +17 -9
- package/dist/universal/.cursor/skills/citable/references/capability-boundaries.md +5 -0
- package/dist/universal/.cursor/skills/citable/references/evidence-authority.md +43 -0
- package/dist/universal/.cursor/skills/citable/schemas/bing-webmaster.schema.json +25 -0
- package/dist/universal/.cursor/skills/citable/schemas/crawler-identity.schema.json +42 -0
- package/dist/universal/.cursor/skills/citable/schemas/evidence-authority.schema.json +15 -0
- package/dist/universal/.cursor/skills/citable/schemas/exception.schema.json +70 -0
- package/dist/universal/.cursor/skills/citable/schemas/observation.schema.json +3 -2
- package/dist/universal/.cursor/skills/citable/schemas/review-policy.schema.json +35 -0
- package/dist/universal/.cursor/skills/citable/schemas/reviewer.schema.json +32 -0
- package/dist/universal/.gemini/skills/citable/SKILL.md +20 -4
- 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/governance.md +17 -0
- package/dist/universal/.gemini/skills/citable/commands/measurement.md +15 -0
- package/dist/universal/.gemini/skills/citable/manifest.json +17 -9
- package/dist/universal/.gemini/skills/citable/references/capability-boundaries.md +5 -0
- package/dist/universal/.gemini/skills/citable/references/evidence-authority.md +43 -0
- package/dist/universal/.gemini/skills/citable/schemas/bing-webmaster.schema.json +25 -0
- package/dist/universal/.gemini/skills/citable/schemas/crawler-identity.schema.json +42 -0
- package/dist/universal/.gemini/skills/citable/schemas/evidence-authority.schema.json +15 -0
- package/dist/universal/.gemini/skills/citable/schemas/exception.schema.json +70 -0
- package/dist/universal/.gemini/skills/citable/schemas/observation.schema.json +3 -2
- package/dist/universal/.gemini/skills/citable/schemas/review-policy.schema.json +35 -0
- package/dist/universal/.gemini/skills/citable/schemas/reviewer.schema.json +32 -0
- package/dist/universal/.github/skills/citable/SKILL.md +20 -4
- 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/governance.md +17 -0
- package/dist/universal/.github/skills/citable/commands/measurement.md +15 -0
- package/dist/universal/.github/skills/citable/manifest.json +17 -9
- package/dist/universal/.github/skills/citable/references/capability-boundaries.md +5 -0
- package/dist/universal/.github/skills/citable/references/evidence-authority.md +43 -0
- package/dist/universal/.github/skills/citable/schemas/bing-webmaster.schema.json +25 -0
- package/dist/universal/.github/skills/citable/schemas/crawler-identity.schema.json +42 -0
- package/dist/universal/.github/skills/citable/schemas/evidence-authority.schema.json +15 -0
- package/dist/universal/.github/skills/citable/schemas/exception.schema.json +70 -0
- package/dist/universal/.github/skills/citable/schemas/observation.schema.json +3 -2
- package/dist/universal/.github/skills/citable/schemas/review-policy.schema.json +35 -0
- package/dist/universal/.github/skills/citable/schemas/reviewer.schema.json +32 -0
- package/dist/universal/.kiro/skills/citable/SKILL.md +20 -4
- 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/governance.md +17 -0
- package/dist/universal/.kiro/skills/citable/commands/measurement.md +15 -0
- package/dist/universal/.kiro/skills/citable/manifest.json +17 -9
- package/dist/universal/.kiro/skills/citable/references/capability-boundaries.md +5 -0
- package/dist/universal/.kiro/skills/citable/references/evidence-authority.md +43 -0
- package/dist/universal/.kiro/skills/citable/schemas/bing-webmaster.schema.json +25 -0
- package/dist/universal/.kiro/skills/citable/schemas/crawler-identity.schema.json +42 -0
- package/dist/universal/.kiro/skills/citable/schemas/evidence-authority.schema.json +15 -0
- package/dist/universal/.kiro/skills/citable/schemas/exception.schema.json +70 -0
- package/dist/universal/.kiro/skills/citable/schemas/observation.schema.json +3 -2
- package/dist/universal/.kiro/skills/citable/schemas/review-policy.schema.json +35 -0
- package/dist/universal/.kiro/skills/citable/schemas/reviewer.schema.json +32 -0
- package/dist/universal/.opencode/skills/citable/SKILL.md +20 -4
- 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/governance.md +17 -0
- package/dist/universal/.opencode/skills/citable/commands/measurement.md +15 -0
- package/dist/universal/.opencode/skills/citable/manifest.json +17 -9
- package/dist/universal/.opencode/skills/citable/references/capability-boundaries.md +5 -0
- package/dist/universal/.opencode/skills/citable/references/evidence-authority.md +43 -0
- package/dist/universal/.opencode/skills/citable/schemas/bing-webmaster.schema.json +25 -0
- package/dist/universal/.opencode/skills/citable/schemas/crawler-identity.schema.json +42 -0
- package/dist/universal/.opencode/skills/citable/schemas/evidence-authority.schema.json +15 -0
- package/dist/universal/.opencode/skills/citable/schemas/exception.schema.json +70 -0
- package/dist/universal/.opencode/skills/citable/schemas/observation.schema.json +3 -2
- package/dist/universal/.opencode/skills/citable/schemas/review-policy.schema.json +35 -0
- package/dist/universal/.opencode/skills/citable/schemas/reviewer.schema.json +32 -0
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +20 -4
- 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/governance.md +17 -0
- package/dist/universal/.pi/agent/skills/citable/commands/measurement.md +15 -0
- package/dist/universal/.pi/agent/skills/citable/manifest.json +17 -9
- package/dist/universal/.pi/agent/skills/citable/references/capability-boundaries.md +5 -0
- package/dist/universal/.pi/agent/skills/citable/references/evidence-authority.md +43 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/bing-webmaster.schema.json +25 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/crawler-identity.schema.json +42 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/evidence-authority.schema.json +15 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/exception.schema.json +70 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/observation.schema.json +3 -2
- package/dist/universal/.pi/agent/skills/citable/schemas/review-policy.schema.json +35 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/reviewer.schema.json +32 -0
- package/dist/universal/.qoder/skills/citable/SKILL.md +20 -4
- 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/governance.md +17 -0
- package/dist/universal/.qoder/skills/citable/commands/measurement.md +15 -0
- package/dist/universal/.qoder/skills/citable/manifest.json +17 -9
- package/dist/universal/.qoder/skills/citable/references/capability-boundaries.md +5 -0
- package/dist/universal/.qoder/skills/citable/references/evidence-authority.md +43 -0
- package/dist/universal/.qoder/skills/citable/schemas/bing-webmaster.schema.json +25 -0
- package/dist/universal/.qoder/skills/citable/schemas/crawler-identity.schema.json +42 -0
- package/dist/universal/.qoder/skills/citable/schemas/evidence-authority.schema.json +15 -0
- package/dist/universal/.qoder/skills/citable/schemas/exception.schema.json +70 -0
- package/dist/universal/.qoder/skills/citable/schemas/observation.schema.json +3 -2
- package/dist/universal/.qoder/skills/citable/schemas/review-policy.schema.json +35 -0
- package/dist/universal/.qoder/skills/citable/schemas/reviewer.schema.json +32 -0
- package/dist/universal/.rovodev/skills/citable/SKILL.md +20 -4
- 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/governance.md +17 -0
- package/dist/universal/.rovodev/skills/citable/commands/measurement.md +15 -0
- package/dist/universal/.rovodev/skills/citable/manifest.json +17 -9
- package/dist/universal/.rovodev/skills/citable/references/capability-boundaries.md +5 -0
- package/dist/universal/.rovodev/skills/citable/references/evidence-authority.md +43 -0
- package/dist/universal/.rovodev/skills/citable/schemas/bing-webmaster.schema.json +25 -0
- package/dist/universal/.rovodev/skills/citable/schemas/crawler-identity.schema.json +42 -0
- package/dist/universal/.rovodev/skills/citable/schemas/evidence-authority.schema.json +15 -0
- package/dist/universal/.rovodev/skills/citable/schemas/exception.schema.json +70 -0
- package/dist/universal/.rovodev/skills/citable/schemas/observation.schema.json +3 -2
- package/dist/universal/.rovodev/skills/citable/schemas/review-policy.schema.json +35 -0
- package/dist/universal/.rovodev/skills/citable/schemas/reviewer.schema.json +32 -0
- package/dist/universal/.trae/skills/citable/SKILL.md +20 -4
- 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/governance.md +17 -0
- package/dist/universal/.trae/skills/citable/commands/measurement.md +15 -0
- package/dist/universal/.trae/skills/citable/manifest.json +17 -9
- package/dist/universal/.trae/skills/citable/references/capability-boundaries.md +5 -0
- package/dist/universal/.trae/skills/citable/references/evidence-authority.md +43 -0
- package/dist/universal/.trae/skills/citable/schemas/bing-webmaster.schema.json +25 -0
- package/dist/universal/.trae/skills/citable/schemas/crawler-identity.schema.json +42 -0
- package/dist/universal/.trae/skills/citable/schemas/evidence-authority.schema.json +15 -0
- package/dist/universal/.trae/skills/citable/schemas/exception.schema.json +70 -0
- package/dist/universal/.trae/skills/citable/schemas/observation.schema.json +3 -2
- package/dist/universal/.trae/skills/citable/schemas/review-policy.schema.json +35 -0
- package/dist/universal/.trae/skills/citable/schemas/reviewer.schema.json +32 -0
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +20 -4
- 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/governance.md +17 -0
- package/dist/universal/.trae-cn/skills/citable/commands/measurement.md +15 -0
- package/dist/universal/.trae-cn/skills/citable/manifest.json +17 -9
- package/dist/universal/.trae-cn/skills/citable/references/capability-boundaries.md +5 -0
- package/dist/universal/.trae-cn/skills/citable/references/evidence-authority.md +43 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/bing-webmaster.schema.json +25 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/crawler-identity.schema.json +42 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/evidence-authority.schema.json +15 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/exception.schema.json +70 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/observation.schema.json +3 -2
- package/dist/universal/.trae-cn/skills/citable/schemas/review-policy.schema.json +35 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/reviewer.schema.json +32 -0
- package/dist/universal/manifest.json +207 -111
- package/package.json +2 -2
- package/schemas/bing-webmaster.schema.json +25 -0
- package/schemas/crawler-identity.schema.json +42 -0
- package/schemas/evidence-authority.schema.json +15 -0
- package/schemas/exception.schema.json +70 -0
- package/schemas/observation.schema.json +3 -2
- package/schemas/review-policy.schema.json +35 -0
- package/schemas/reviewer.schema.json +32 -0
- package/skill/SKILL.md +20 -4
- package/skill/commands/README.md +1 -0
- package/skill/commands/governance.md +17 -0
- package/skill/commands/measurement.md +15 -0
- package/skill/references/capability-boundaries.md +5 -0
- package/skill/references/evidence-authority.md +43 -0
- package/src/cli/index.js +20 -1
- package/src/commands/governance.js +144 -0
- package/src/commands/observe.js +81 -22
- package/src/commands/selfUpgrade.js +1 -1
- package/src/observations/authority.js +13 -0
- package/src/observations/common.js +3 -1
- package/src/observations/crawlerIdentity.js +60 -0
- package/src/registries/index.js +17 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createRun } from '../evidence/run.js';
|
|
4
|
+
import { loadRegistries, checkReferentialIntegrity } from '../registries/index.js';
|
|
5
|
+
import { readJson, sha256, parseRefDate } from '../shared/io.js';
|
|
6
|
+
|
|
7
|
+
function recordHash(value) {
|
|
8
|
+
return sha256(JSON.stringify(value));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function assigned(exception, role) {
|
|
12
|
+
return exception.reviewer_assignments.filter((item) => item.role === role).map((item) => item.reviewer_id);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function reviewerCanAct(reviewer, role, finding) {
|
|
16
|
+
if (!reviewer || reviewer.status !== 'active' || !reviewer.roles.includes(role)) return false;
|
|
17
|
+
const scopes = reviewer.authorized_scopes || [];
|
|
18
|
+
return scopes.includes('*') || scopes.includes(finding.detector_id) || scopes.includes(finding.subject.identifier);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function exceptionProblems(exception, policy, reviewers, findings, evidence, refDate) {
|
|
22
|
+
const problems = [];
|
|
23
|
+
if (!policy || policy.status !== 'active') problems.push('review policy is missing or inactive');
|
|
24
|
+
if (exception.status !== 'approved') problems.push(`exception status is ${exception.status}`);
|
|
25
|
+
if (exception.residual_risk !== 'documented') problems.push('residual risk is not documented');
|
|
26
|
+
if (new Date(exception.expires_at) < refDate) problems.push('exception is expired');
|
|
27
|
+
if (exception.renewal_count > exception.renewal_limit) problems.push('exception renewal limit exceeded');
|
|
28
|
+
if (policy && exception.renewal_limit > policy.max_renewals) problems.push('exception renewal limit exceeds policy');
|
|
29
|
+
if (policy && new Date(exception.expires_at) - new Date(exception.created_at) > policy.max_exception_days * 86400000) problems.push('exception duration exceeds policy');
|
|
30
|
+
if (policy && recordHash(policy) !== exception.policy_hash) problems.push('policy changed since approval');
|
|
31
|
+
if (exception.superseded_by_exception_id) problems.push('exception is superseded');
|
|
32
|
+
const owner = reviewers.get(exception.owner_reviewer_id);
|
|
33
|
+
if (!owner || owner.status !== 'active') problems.push('exception owner is missing or inactive');
|
|
34
|
+
|
|
35
|
+
for (const findingId of exception.finding_ids) {
|
|
36
|
+
const finding = findings.get(findingId);
|
|
37
|
+
if (!finding) problems.push(`source finding ${findingId} is unavailable`);
|
|
38
|
+
else if (recordHash(finding) !== exception.finding_hashes[findingId]) problems.push(`source finding ${findingId} changed since approval`);
|
|
39
|
+
}
|
|
40
|
+
for (const evidenceId of exception.evidence_ids) {
|
|
41
|
+
const item = evidence.get(evidenceId);
|
|
42
|
+
if (!item) problems.push(`evidence ${evidenceId} is unavailable`);
|
|
43
|
+
else if (recordHash(item) !== exception.evidence_hashes[evidenceId]) problems.push(`evidence ${evidenceId} changed since approval`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (policy) {
|
|
47
|
+
for (const role of policy.required_roles) {
|
|
48
|
+
const actors = assigned(exception, role);
|
|
49
|
+
if (actors.length === 0) problems.push(`required role ${role} is unassigned`);
|
|
50
|
+
for (const actor of actors) {
|
|
51
|
+
const reviewer = reviewers.get(actor);
|
|
52
|
+
const scoped = exception.finding_ids.every((id) => findings.has(id) && reviewerCanAct(reviewer, role, findings.get(id)));
|
|
53
|
+
if (!scoped) problems.push(`${actor} is not active and authorized for role ${role} across the exception scope`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const authors = new Set(assigned(exception, 'content_author'));
|
|
57
|
+
const implementers = new Set(assigned(exception, 'remediation_implementer'));
|
|
58
|
+
const verifiers = new Set([...assigned(exception, 'technical_reviewer'), ...assigned(exception, 'subject_matter_reviewer'), ...assigned(exception, 'independent_reviewer')]);
|
|
59
|
+
if (policy.separation_rules.includes('author_cannot_verify_own_claim') && [...authors].some((id) => verifiers.has(id))) problems.push('content author also verifies the scoped finding');
|
|
60
|
+
if (policy.separation_rules.includes('implementer_cannot_solely_verify') && verifiers.size === 1 && [...implementers].some((id) => verifiers.has(id))) problems.push('remediation implementer is the sole verifier');
|
|
61
|
+
if (policy.separation_rules.includes('commercial_owner_requires_independent_benefit_review') && exception.reviewer_independence !== 'established') problems.push('independent review is not established');
|
|
62
|
+
if (policy.separation_rules.includes('risk_acceptor_must_be_authorized') && assigned(exception, 'risk_acceptor').length === 0) problems.push('authorized risk acceptor is missing');
|
|
63
|
+
}
|
|
64
|
+
for (const assignment of exception.reviewer_assignments) {
|
|
65
|
+
const reviewer = reviewers.get(assignment.reviewer_id);
|
|
66
|
+
if (!reviewer) continue;
|
|
67
|
+
if (reviewer.status !== 'active' || !reviewer.roles.includes(assignment.role)) problems.push(`${reviewer.reviewer_id} is not active in assigned role ${assignment.role}`);
|
|
68
|
+
if ((reviewer.conflicts || []).some((scope) => exception.finding_ids.some((id) => findings.get(id)?.subject.identifier === scope))) problems.push(`${reviewer.reviewer_id} has a declared conflict with the exception scope`);
|
|
69
|
+
}
|
|
70
|
+
return [...new Set(problems)];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function validateGovernance(root, { refDate } = {}) {
|
|
74
|
+
const { registries, problems: schemaProblems } = loadRegistries(root);
|
|
75
|
+
const problems = [...schemaProblems, ...checkReferentialIntegrity(registries)];
|
|
76
|
+
const reference = parseRefDate(refDate);
|
|
77
|
+
const reviewers = new Map(registries.reviewers.entries.map((item) => [item.reviewer_id, item]));
|
|
78
|
+
const policies = new Map(registries.review_policies.entries.map((item) => [item.policy_id, item]));
|
|
79
|
+
const evidence = new Map(registries.evidence.entries.map((item) => [item.evidence_id, item]));
|
|
80
|
+
const activeByFinding = new Map();
|
|
81
|
+
for (const exception of registries.exceptions.entries) {
|
|
82
|
+
const file = path.join(root, '.citable', 'runs', exception.source_run_id, 'findings.json');
|
|
83
|
+
const findings = new Map(fs.existsSync(file) ? readJson(file).map((item) => [item.finding_id, item]) : []);
|
|
84
|
+
const exceptionIssues = exceptionProblems(exception, policies.get(exception.policy_id), reviewers, findings, evidence, reference);
|
|
85
|
+
problems.push(...exceptionIssues.map((problem) => `exceptions/${exception.exception_id}: ${problem}`));
|
|
86
|
+
if (exceptionIssues.length === 0) {
|
|
87
|
+
for (const findingId of exception.finding_ids) {
|
|
88
|
+
const key = `${exception.source_run_id}:${findingId}`;
|
|
89
|
+
activeByFinding.set(key, [...(activeByFinding.get(key) || []), exception.exception_id]);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const [scope, exceptionIds] of activeByFinding) {
|
|
94
|
+
if (exceptionIds.length > 1) problems.push(`ambiguous active exceptions for ${scope}: ${exceptionIds.join(', ')}`);
|
|
95
|
+
}
|
|
96
|
+
return { ok: problems.length === 0, problems, counts: { reviewers: reviewers.size, policies: policies.size, exceptions: registries.exceptions.entries.length } };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function evaluateDispositions(root, { runId, refDate } = {}) {
|
|
100
|
+
if (!runId) throw new Error('source run id is required');
|
|
101
|
+
const findingsFile = path.join(root, '.citable', 'runs', runId, 'findings.json');
|
|
102
|
+
if (!fs.existsSync(findingsFile)) throw new Error(`source findings not found for run ${runId}`);
|
|
103
|
+
const sourceBytes = fs.readFileSync(findingsFile);
|
|
104
|
+
const sourceFindings = JSON.parse(sourceBytes);
|
|
105
|
+
const findings = new Map(sourceFindings.map((item) => [item.finding_id, item]));
|
|
106
|
+
const { registries, problems } = loadRegistries(root);
|
|
107
|
+
const integrity = checkReferentialIntegrity(registries);
|
|
108
|
+
if (problems.length || integrity.length) throw new Error(`registry validation failed: ${[...problems, ...integrity].join('; ')}`);
|
|
109
|
+
const reviewers = new Map(registries.reviewers.entries.map((item) => [item.reviewer_id, item]));
|
|
110
|
+
const policies = new Map(registries.review_policies.entries.map((item) => [item.policy_id, item]));
|
|
111
|
+
const evidence = new Map(registries.evidence.entries.map((item) => [item.evidence_id, item]));
|
|
112
|
+
const reference = parseRefDate(refDate);
|
|
113
|
+
const applicable = registries.exceptions.entries.filter((item) => item.source_run_id === runId);
|
|
114
|
+
const evaluations = applicable.map((exception) => {
|
|
115
|
+
const exceptionProblemsList = exceptionProblems(exception, policies.get(exception.policy_id), reviewers, findings, evidence, reference);
|
|
116
|
+
return { exception, validity: exceptionProblemsList.length ? 'invalid' : 'active', problems: exceptionProblemsList };
|
|
117
|
+
});
|
|
118
|
+
const dispositions = sourceFindings.map((finding) => {
|
|
119
|
+
const matches = evaluations.filter((item) => item.exception.finding_ids.includes(finding.finding_id));
|
|
120
|
+
const active = matches.filter((item) => item.validity === 'active');
|
|
121
|
+
return {
|
|
122
|
+
finding_id: finding.finding_id,
|
|
123
|
+
detector_id: finding.detector_id,
|
|
124
|
+
subject: finding.subject,
|
|
125
|
+
technical_state: 'failed',
|
|
126
|
+
enforcement_disposition: active.length === 1 ? 'accepted_exception' : active.length > 1 ? 'blocked_ambiguous_exception' : 'enforce',
|
|
127
|
+
exception_validity: active.length === 1 ? 'active' : matches.length ? (active.length > 1 ? 'ambiguous' : 'invalid') : 'not_applicable',
|
|
128
|
+
exception_id: active.length === 1 ? active[0].exception.exception_id : null,
|
|
129
|
+
residual_risk: active.length === 1 ? active[0].exception.residual_risk : 'not_documented',
|
|
130
|
+
problems: matches.flatMap((item) => item.problems),
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
const run = createRun(root, { command: 'governance evaluate', argv: [runId], target: { kind: 'registries', location: findingsFile } });
|
|
134
|
+
run.addInput('source-findings', sourceBytes);
|
|
135
|
+
run.addInput('exceptions', registries.exceptions);
|
|
136
|
+
run.addInput('review-policies', registries.review_policies);
|
|
137
|
+
run.writeArtifact('dispositions.json', dispositions);
|
|
138
|
+
run.writeArtifact('exception-evaluations.json', evaluations.map(({ exception, ...evaluation }) => ({ exception_id: exception.exception_id, ...evaluation })));
|
|
139
|
+
run.manifest.warnings.push('Accepted exceptions alter enforcement disposition only; source findings remain failed and immutable.');
|
|
140
|
+
run.finalize(dispositions.some((item) => item.enforcement_disposition === 'blocked_ambiguous_exception') ? 'incomplete' : 'completed_with_warnings');
|
|
141
|
+
return { runId: run.runId, dir: run.dir, source_run_id: runId, dispositions, exception_evaluations: evaluations.length };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export { recordHash };
|
package/src/commands/observe.js
CHANGED
|
@@ -2,10 +2,15 @@ import { buildContext } from './context.js';
|
|
|
2
2
|
import { envelope, observationRun, readInput } from '../observations/common.js';
|
|
3
3
|
import { sha256 } from '../shared/io.js';
|
|
4
4
|
import { fetchUrl } from '../crawler/fetch.js';
|
|
5
|
-
import
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { parse as parseCsv } from 'csv-parse/sync';
|
|
8
|
+
import { crawlerIdentity } from '../observations/crawlerIdentity.js';
|
|
9
|
+
import { validateAgainst } from '../shared/schemaValidator.js';
|
|
6
10
|
|
|
7
11
|
const originOf = (value) => { try { return new URL(value).origin; } catch { return null; } };
|
|
8
12
|
const words = (text) => String(text || '').trim().split(/\s+/).filter(Boolean);
|
|
13
|
+
const strictDate = (value) => /^\d{4}-\d{2}-\d{2}$/.test(value || '') && new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value;
|
|
9
14
|
|
|
10
15
|
function canonicalReview(raw, targetOrigin) {
|
|
11
16
|
const citations = raw.citations || [];
|
|
@@ -125,32 +130,85 @@ async function observeCitations(root, options) {
|
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
function observeLogs(root, options) {
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
133
|
+
if (!options.input || !fs.existsSync(options.input)) throw new Error('logs requires --input <json|csv>');
|
|
134
|
+
const raw = fs.readFileSync(options.input, 'utf8'), ext = path.extname(options.input).toLowerCase();
|
|
135
|
+
const value = ext === '.csv' ? { requests: parseCsv(raw, { columns: true, skip_empty_lines: true, trim: true }) } : JSON.parse(raw);
|
|
136
|
+
const input = { raw, value, file: path.resolve(options.input) };
|
|
137
|
+
const rows = (Array.isArray(value) ? value : value.requests || []).map((row) => ({
|
|
138
|
+
...row,
|
|
139
|
+
timestamp: row.timestamp || row.datetime || row.date,
|
|
140
|
+
url: row.url || row.request_url || row['cs-uri-stem'],
|
|
141
|
+
user_agent: row.user_agent || row.userAgent || row['cs(User-Agent)'],
|
|
142
|
+
source_ip: row.source_ip || row.clientIP || row['c-ip'],
|
|
143
|
+
status: Number(row.status || row.status_code || row['sc-status']),
|
|
144
|
+
bytes: row.bytes == null ? null : Number(row.bytes),
|
|
145
|
+
latency_ms: row.latency_ms == null ? null : Number(row.latency_ms),
|
|
146
|
+
edge_observed: row.edge_observed === true || row.edge_observed === 'true',
|
|
147
|
+
origin_observed: row.origin_observed === true || row.origin_observed === 'true',
|
|
148
|
+
origin_status: row.origin_status == null || row.origin_status === '' ? null : Number(row.origin_status),
|
|
149
|
+
claimed_verified: row.claimed_verified === true || row.claimed_verified === 'true',
|
|
150
|
+
}));
|
|
151
|
+
const sensitive = (Array.isArray(value) ? value : value.requests || []).flatMap((row) => Object.keys(row).filter((key) => /(^|[-_])(authorization|cookie|set-cookie|access[-_]?token|refresh[-_]?token|api[-_]?key|password|secret)($|[-_])/i.test(key)));
|
|
152
|
+
if (sensitive.length) throw new Error(`log import contains sensitive fields that must be removed before collection: ${[...new Set(sensitive)].join(', ')}`);
|
|
153
|
+
const ranges = value.provider_ranges || {}, rangeSources = value.range_sources || {}, metadata = value.metadata || {};
|
|
144
154
|
const observations = rows.map((row) => {
|
|
145
|
-
|
|
155
|
+
if (!row.timestamp || Number.isNaN(Date.parse(row.timestamp)) || !row.url || !row.user_agent || !Number.isInteger(row.status)) throw new Error('each log row requires valid timestamp, url, user_agent, and integer status');
|
|
156
|
+
const identity = crawlerIdentity(row, { ranges, rangeSources, collector: metadata.collector || 'owner_import' });
|
|
157
|
+
const fullyVerified = identity.verification_status === 'fully_verified';
|
|
146
158
|
return envelope('crawler_log', {
|
|
147
159
|
timestamp: row.timestamp, url: row.url, user_agent: row.user_agent, source_ip: row.source_ip,
|
|
148
160
|
status: row.status, bytes: row.bytes ?? null, latency_ms: row.latency_ms ?? null,
|
|
149
161
|
cache_status: row.cache_status ?? null, region: row.region ?? null,
|
|
150
|
-
|
|
151
|
-
}, { method: 'owner_import', source: input.file, raw: JSON.stringify(row), confidence:
|
|
162
|
+
crawler_identity: identity,
|
|
163
|
+
}, { method: 'owner_import', source: input.file, raw: JSON.stringify(row), confidence: fullyVerified ? 'confirmed' : identity.verification_status === 'contradictory' ? 'high' : 'low',
|
|
164
|
+
authority: { collection_authority: 'production_log', authenticity_status: fullyVerified ? 'provider_range_verified' : 'checksum_protected_only', representativeness: metadata.representativeness || 'unknown' },
|
|
165
|
+
limitations: fullyVerified ? ['Verification applies only to this imported production event and captured verification chain.'] : ['The crawler identity chain is incomplete or contradictory; user agent and CIDR matching alone are insufficient.'] });
|
|
166
|
+
});
|
|
167
|
+
return observationRun(root, 'observe logs', input.file, observations, { rawInputs: { server_logs: input.raw }, incomplete: observations.some((o) => o.data.crawler_identity.verification_status !== 'fully_verified') ? ['Some crawler identities are not fully verified.'] : [] });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function observeBing(root, options) {
|
|
171
|
+
if (!options.input || !fs.existsSync(options.input)) throw new Error('bing requires --input <json|csv> --dataset <search_performance|ai_performance>');
|
|
172
|
+
if (!['search_performance', 'ai_performance'].includes(options.dataset)) throw new Error('bing dataset must be search_performance or ai_performance');
|
|
173
|
+
const raw = fs.readFileSync(options.input, 'utf8'), ext = path.extname(options.input).toLowerCase();
|
|
174
|
+
const value = ext === '.csv' ? parseCsv(raw, { columns: true, skip_empty_lines: true, trim: true }) : JSON.parse(raw);
|
|
175
|
+
const document = Array.isArray(value) ? { rows: value } : value;
|
|
176
|
+
const rows = document.rows || document.items || [];
|
|
177
|
+
if (!rows.length) throw new Error('Bing owner export contains no rows');
|
|
178
|
+
const numeric = (row, ...names) => {
|
|
179
|
+
const rawValue = names.map((name) => row[name]).find((item) => item !== '' && item != null);
|
|
180
|
+
if (rawValue == null) return null;
|
|
181
|
+
const result = Number(String(rawValue).replace(/%$/, ''));
|
|
182
|
+
if (!Number.isFinite(result)) throw new Error(`Bing metric ${names[0]} must be numeric`);
|
|
183
|
+
return String(rawValue).endsWith('%') ? result / 100 : result;
|
|
184
|
+
};
|
|
185
|
+
const observations = rows.map((row) => {
|
|
186
|
+
const date = row.date || row.Date;
|
|
187
|
+
if (!strictDate(date)) throw new Error('each Bing row requires a valid date in YYYY-MM-DD format');
|
|
188
|
+
const metrics = options.dataset === 'ai_performance' ? {
|
|
189
|
+
total_citations: numeric(row, 'total_citations', 'Total Citations', 'citations'),
|
|
190
|
+
average_cited_pages: numeric(row, 'average_cited_pages', 'Average Cited Pages'),
|
|
191
|
+
} : {
|
|
192
|
+
clicks: numeric(row, 'clicks', 'Clicks'), impressions: numeric(row, 'impressions', 'Impressions'),
|
|
193
|
+
ctr: numeric(row, 'ctr', 'CTR', 'Average CTR'), position: numeric(row, 'position', 'Average Position'),
|
|
194
|
+
crawl_requests: numeric(row, 'crawl_requests', 'Crawl Requests'), crawl_errors: numeric(row, 'crawl_errors', 'Crawl Errors'),
|
|
195
|
+
indexed_pages: numeric(row, 'indexed_pages', 'Indexed Pages'),
|
|
196
|
+
};
|
|
197
|
+
const boundaries = options.dataset === 'ai_performance'
|
|
198
|
+
? ['Citation counts do not indicate ranking, authority, placement, page importance, or material support.', 'Grounding queries are sampled and the dashboard aggregates supported Microsoft AI surfaces.']
|
|
199
|
+
: ['Bing search performance can combine traffic sources; source dimensions must be preserved.', 'Observed performance does not establish that an intervention caused a change.'];
|
|
200
|
+
if (!Object.values(metrics).some((value) => value != null)) throw new Error(`Bing ${options.dataset} row contains no recognized metrics`);
|
|
201
|
+
const data = {
|
|
202
|
+
dataset: options.dataset, date, url: row.url || row.URL || row.page || null, metrics,
|
|
203
|
+
dimensions: { query: row.query || row.Query || null, grounding_query: row.grounding_query || row['Grounding Query'] || null, source: row.source || row.Source || null, country: row.country || row.Country || null, device: row.device || row.Device || null },
|
|
204
|
+
interpretation_boundary: boundaries,
|
|
205
|
+
};
|
|
206
|
+
const check = validateAgainst('bing-webmaster.schema.json', data);
|
|
207
|
+
if (!check.valid) throw new Error(`Bing observation violates contract: ${check.errors.join('; ')}`);
|
|
208
|
+
return envelope('bing_webmaster', data, { method: 'owner_import', source: path.resolve(options.input), raw: JSON.stringify(row),
|
|
209
|
+
authority: { representativeness: document.metadata?.representativeness || 'unknown' }, limitations: boundaries });
|
|
152
210
|
});
|
|
153
|
-
return observationRun(root, 'observe
|
|
211
|
+
return observationRun(root, 'observe bing', path.resolve(options.input), observations, { rawInputs: { bing_owner_export: raw }, warnings: ['Bing AI Performance has no supported API contract captured by this release; this evidence is an owner export.'] });
|
|
154
212
|
}
|
|
155
213
|
|
|
156
214
|
async function observePassages(root, options) {
|
|
@@ -226,10 +284,11 @@ export async function observe(root, mode, options = {}) {
|
|
|
226
284
|
case 'index': return observeIndex(root, options);
|
|
227
285
|
case 'citations': return observeCitations(root, options);
|
|
228
286
|
case 'logs': return observeLogs(root, options);
|
|
287
|
+
case 'bing': return observeBing(root, options);
|
|
229
288
|
case 'passages': return observePassages(root, options);
|
|
230
289
|
case 'consensus': return observeConsensus(root, options);
|
|
231
290
|
case 'performance': return observePerformance(root, options);
|
|
232
291
|
case 'corroboration': return observeCorroboration(root, options);
|
|
233
|
-
default: throw new Error('observe mode must be render, index, citations, logs, passages, consensus, performance, or corroboration');
|
|
292
|
+
default: throw new Error('observe mode must be render, index, citations, logs, bing, passages, consensus, performance, or corroboration');
|
|
234
293
|
}
|
|
235
294
|
}
|
|
@@ -21,7 +21,7 @@ const FETCH_TIMEOUT_MS = 10_000;
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Fetch the latest published version from the npm registry.
|
|
24
|
-
* Uses Node's built-in fetch (Node
|
|
24
|
+
* Uses Node's built-in fetch (Node 24+); no extra dependencies.
|
|
25
25
|
*
|
|
26
26
|
* @returns {Promise<string>} latest semver string
|
|
27
27
|
*/
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const DEFAULTS = {
|
|
2
|
+
live_api: ['provider_published', 'direct_api', 'transport_authenticated', 'unknown'],
|
|
3
|
+
browser: ['synthetic', 'browser_capture', 'checksum_protected_only', 'single_observation'],
|
|
4
|
+
owner_import: ['owner_controlled', 'owner_export', 'checksum_protected_only', 'unknown'],
|
|
5
|
+
synthetic_fetch: ['synthetic', 'synthetic_probe', 'checksum_protected_only', 'single_observation'],
|
|
6
|
+
static_analysis: ['inferred', 'static_analysis', 'checksum_protected_only', 'single_observation'],
|
|
7
|
+
human_review: ['owner_controlled', 'manual_entry', 'owner_attested', 'single_observation'],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function evidenceAuthority(method, overrides = {}) {
|
|
11
|
+
const [source_authority, collection_authority, authenticity_status, representativeness] = DEFAULTS[method] || DEFAULTS.static_analysis;
|
|
12
|
+
return { source_authority, collection_authority, authenticity_status, representativeness, ...overrides };
|
|
13
|
+
}
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { createRun } from '../evidence/run.js';
|
|
4
4
|
import { nowIso, readJson, sha256 } from '../shared/io.js';
|
|
5
5
|
import { validateAgainst } from '../shared/schemaValidator.js';
|
|
6
|
+
import { evidenceAuthority } from './authority.js';
|
|
6
7
|
|
|
7
8
|
export function readInput(input) {
|
|
8
9
|
if (!input) throw new Error('--input <json> is required for this collector');
|
|
@@ -10,12 +11,13 @@ export function readInput(input) {
|
|
|
10
11
|
return { raw: fs.readFileSync(input, 'utf8'), value: readJson(input), file: path.resolve(input) };
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export function envelope(kind, data, { method, source, state = 'observed', confidence = 'confirmed', limitations = [], raw } = {}) {
|
|
14
|
+
export function envelope(kind, data, { method, source, state = 'observed', confidence = 'confirmed', limitations = [], authority, raw } = {}) {
|
|
14
15
|
const evidence = raw ?? JSON.stringify(data);
|
|
15
16
|
const item = {
|
|
16
17
|
observation_id: `OBS-${kind.toUpperCase()}-${sha256(evidence).slice(0, 16)}`,
|
|
17
18
|
kind, state, collected_at: nowIso(), collection_method: method,
|
|
18
19
|
confidence, source, evidence_hash: sha256(evidence), data, limitations,
|
|
20
|
+
authority: evidenceAuthority(method, authority),
|
|
19
21
|
};
|
|
20
22
|
const check = validateAgainst('observation.schema.json', item);
|
|
21
23
|
if (!check.valid) throw new Error(`observation violates contract: ${check.errors.join('; ')}`);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import { sha256 } from '../shared/io.js';
|
|
3
|
+
import { validateAgainst } from '../shared/schemaValidator.js';
|
|
4
|
+
|
|
5
|
+
function cidrMembership(sourceIp, cidrs) {
|
|
6
|
+
if (!sourceIp) return 'not_tested';
|
|
7
|
+
const family = net.isIP(sourceIp);
|
|
8
|
+
if (!family) return 'invalid';
|
|
9
|
+
const block = new net.BlockList();
|
|
10
|
+
for (const cidr of cidrs || []) {
|
|
11
|
+
const [network, prefixText] = String(cidr).split('/');
|
|
12
|
+
const prefix = Number(prefixText), rangeFamily = net.isIP(network);
|
|
13
|
+
if (rangeFamily === family && Number.isInteger(prefix)) block.addSubnet(network, prefix, family === 4 ? 'ipv4' : 'ipv6');
|
|
14
|
+
}
|
|
15
|
+
return block.check(sourceIp, family === 4 ? 'ipv4' : 'ipv6') ? 'matched' : 'not_matched';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function crawlerIdentity(row, { ranges = {}, rangeSources = {}, collector = 'owner_import' } = {}) {
|
|
19
|
+
const ua = row.user_agent, cidrs = ranges[ua] || [], source = rangeSources[ua] || {};
|
|
20
|
+
const sourceSnapshot = source.source_snapshot;
|
|
21
|
+
const expectedChecksum = sourceSnapshot == null ? null : sha256(typeof sourceSnapshot === 'string' ? sourceSnapshot : JSON.stringify(sourceSnapshot));
|
|
22
|
+
const rangeSource = {
|
|
23
|
+
url: source.url || null,
|
|
24
|
+
retrieved_at: source.retrieved_at || null,
|
|
25
|
+
authority: source.authority || 'unknown',
|
|
26
|
+
checksum: source.checksum || null,
|
|
27
|
+
checksum_scope: sourceSnapshot == null ? 'not_captured' : 'source_snapshot',
|
|
28
|
+
checksum_valid: Boolean(source.checksum && expectedChecksum && source.checksum === expectedChecksum),
|
|
29
|
+
};
|
|
30
|
+
const membership = cidrMembership(row.source_ip, cidrs);
|
|
31
|
+
const dns = { status: row.dns_status || 'not_tested', method: row.dns_method || null };
|
|
32
|
+
const edge = { observed: row.edge_observed === true, decision: row.edge_decision || null };
|
|
33
|
+
const origin = { observed: row.origin_observed === true, status: row.origin_status ?? null };
|
|
34
|
+
const stages = ['declared'];
|
|
35
|
+
if (row.synthetic === true) stages.push('synthetically_observed');
|
|
36
|
+
if (membership === 'matched') stages.push('range_matched');
|
|
37
|
+
if (dns.status === 'verified') stages.push('dns_verified');
|
|
38
|
+
if (edge.observed) stages.push('edge_observed');
|
|
39
|
+
if (origin.observed) stages.push('origin_observed');
|
|
40
|
+
const contradictory = membership === 'not_matched' || dns.status === 'failed' || (row.claimed_verified === true && (!rangeSource.checksum_valid || membership !== 'matched'));
|
|
41
|
+
const complete = identityProvider(row, source) !== 'unknown' && row.synthetic !== true && rangeSource.url && rangeSource.retrieved_at && rangeSource.authority === 'provider_published' && rangeSource.checksum_scope === 'source_snapshot' && rangeSource.checksum_valid && membership === 'matched' && dns.status === 'verified' && edge.observed && origin.observed;
|
|
42
|
+
if (contradictory) stages.push('contradictory');
|
|
43
|
+
else if (complete) stages.push('fully_verified');
|
|
44
|
+
else stages.push('insufficient_evidence');
|
|
45
|
+
const precedence = ['contradictory', 'fully_verified', 'origin_observed', 'edge_observed', 'dns_verified', 'range_matched', 'synthetically_observed', 'declared'];
|
|
46
|
+
const verificationStatus = precedence.find((state) => stages.includes(state));
|
|
47
|
+
const identity = {
|
|
48
|
+
provider: identityProvider(row, source), declared_user_agent: ua,
|
|
49
|
+
observed_source_ip: row.source_ip || null, range_source: rangeSource,
|
|
50
|
+
cidr_membership: membership, dns, edge, origin, region: row.region || null,
|
|
51
|
+
collector, stages, verification_status: verificationStatus,
|
|
52
|
+
};
|
|
53
|
+
const check = validateAgainst('crawler-identity.schema.json', identity);
|
|
54
|
+
if (!check.valid) throw new Error(`crawler identity violates contract: ${check.errors.join('; ')}`);
|
|
55
|
+
return identity;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function identityProvider(row, source) {
|
|
59
|
+
return row.provider || source.provider || 'unknown';
|
|
60
|
+
}
|
package/src/registries/index.js
CHANGED
|
@@ -17,6 +17,9 @@ export const REGISTRY_SPECS = [
|
|
|
17
17
|
{ file: 'objectives.yaml', kind: 'objectives', schema: 'objective.schema.json', idField: 'objective_id' },
|
|
18
18
|
{ file: 'interventions.yaml', kind: 'interventions', schema: 'intervention.schema.json', idField: 'intervention_id' },
|
|
19
19
|
{ file: 'connections.yaml', kind: 'connections', schema: 'connection.schema.json', idField: 'connection_id' },
|
|
20
|
+
{ file: 'reviewers.yaml', kind: 'reviewers', schema: 'reviewer.schema.json', idField: 'reviewer_id' },
|
|
21
|
+
{ file: 'review-policies.yaml', kind: 'review_policies', schema: 'review-policy.schema.json', idField: 'policy_id' },
|
|
22
|
+
{ file: 'exceptions.yaml', kind: 'exceptions', schema: 'exception.schema.json', idField: 'exception_id' },
|
|
20
23
|
];
|
|
21
24
|
|
|
22
25
|
export function contextDir(root) {
|
|
@@ -104,6 +107,20 @@ export function checkReferentialIntegrity(registries) {
|
|
|
104
107
|
for (const intervention of registries.interventions?.entries || []) {
|
|
105
108
|
for (const ref of intervention.objective_ids || []) if (!objectiveIds.has(ref)) problems.push(`interventions/${intervention.intervention_id}: references unknown objective id "${ref}"`);
|
|
106
109
|
}
|
|
110
|
+
const reviewerIds = ids.reviewers || new Set();
|
|
111
|
+
const policyIds = ids.review_policies || new Set();
|
|
112
|
+
const evidenceIds = ids.evidence || new Set();
|
|
113
|
+
const interventionIds = ids.interventions || new Set();
|
|
114
|
+
const exceptionIds = ids.exceptions || new Set();
|
|
115
|
+
for (const exception of registries.exceptions?.entries || []) {
|
|
116
|
+
const prefix = `exceptions/${exception.exception_id}`;
|
|
117
|
+
if (!policyIds.has(exception.policy_id)) problems.push(`${prefix}: references unknown review policy id "${exception.policy_id}"`);
|
|
118
|
+
const reviewerRefs = [exception.owner_reviewer_id, ...(exception.reviewer_assignments || []).map((item) => item.reviewer_id), ...(exception.audit_history || []).map((item) => item.actor_reviewer_id)];
|
|
119
|
+
for (const ref of reviewerRefs) if (ref && !reviewerIds.has(ref)) problems.push(`${prefix}: references unknown reviewer id "${ref}"`);
|
|
120
|
+
for (const ref of exception.evidence_ids || []) if (!evidenceIds.has(ref)) problems.push(`${prefix}: references unknown evidence id "${ref}"`);
|
|
121
|
+
if (exception.related_intervention_id && !interventionIds.has(exception.related_intervention_id)) problems.push(`${prefix}: references unknown intervention id "${exception.related_intervention_id}"`);
|
|
122
|
+
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
|
+
}
|
|
107
124
|
return problems;
|
|
108
125
|
}
|
|
109
126
|
|