amogaddy's picture
Integra World Monitor (AGPL-3.0, self-hosted) nello Space: pagina, menu, e arricchimento notizie per la AI (part 4)
ee888e1 verified
Raw
History Blame Contribute Delete
23.6 kB
import { createHash } from 'node:crypto';
export const CHINA_POLICY_AGENCIES = Object.freeze([
'CAC',
'SAMR',
'MIIT',
'MOFCOM',
'NDRC',
'PBOC',
]);
const PUBLISHERS = Object.freeze({
CAC: {
id: 'publisher:cac-cn',
name: 'Cyberspace Administration of China',
sourceName: 'CAC (China)',
},
SAMR: {
id: 'publisher:samr-cn',
name: 'State Administration for Market Regulation',
sourceName: 'SAMR (China)',
},
MIIT: {
id: 'publisher:miit-cn',
name: 'Ministry of Industry and Information Technology',
sourceName: 'MIIT (China)',
},
MOFCOM: {
id: 'publisher:mofcom-cn',
name: 'Ministry of Commerce',
sourceName: 'MOFCOM (China)',
},
NDRC: {
id: 'publisher:ndrc-cn',
name: 'National Development and Reform Commission',
sourceName: 'NDRC (China)',
},
PBOC: {
id: 'publisher:pboc-cn',
name: "People's Bank of China",
sourceName: 'PBoC (China)',
},
});
const SECTOR_RULES = Object.freeze([
{ id: 'data-security', label: 'Data security', pattern: /数据安全|个人信息|网络数据/ },
{ id: 'digital-platforms', label: 'Digital platforms', pattern: /平台经济|网络平台|互联网平台/ },
{ id: 'telecommunications', label: 'Telecommunications', pattern: /电信|通信|IPv6|互联网协议/ },
{ id: 'industrial-policy', label: 'Industrial policy', pattern: /工业政策|制造业|制造|工厂|产业结构|产业政策/ },
{ id: 'trade-export-controls', label: 'Trade and export controls', pattern: /出口管制|反倾销|进口|出口|外贸/ },
{ id: 'financial-services', label: 'Financial services', pattern: /银行|金融|人民币|反洗钱|支付/ },
{ id: 'consumer-markets', label: 'Consumer markets', pattern: /消费者|产品质量|食品安全|广告/ },
{ id: 'competition', label: 'Competition', pattern: /反垄断|竞争政策|经营者集中/ },
{ id: 'energy-transition', label: 'Energy transition', pattern: /零碳|节能|能源|碳排放/ },
]);
const REVIEWED_ENTITIES = Object.freeze([
{ id: 'org:alibaba-china', name: '阿里巴巴(中国)有限公司' },
{ id: 'org:tencent-technology-shenzhen', name: '腾讯科技(深圳)有限公司' },
]);
const PROVENANCE_DIMENSIONS = Object.freeze([
'publisher',
'source_url',
'original_reference',
'original_language',
'translation',
'observation_time',
'effective_time',
'publication_time',
'retrieval_time',
'revision',
'supersession',
'extraction_confidence',
'classification_confidence',
'corroboration',
'transport_freshness',
'content_freshness',
'derivation',
]);
const ACTION_TYPES = Object.freeze([
'consultation_draft',
'announcement_guidance',
'final_rule',
'administrative_approval_restriction',
'investigation',
'enforcement_penalty',
'effective_date_change',
'amendment_correction_repeal_supersession',
'unknown',
]);
const POLICY_STATUSES = Object.freeze([
'draft',
'announced',
'in_force',
'under_investigation',
'enforced',
'amended',
'corrected',
'repealed',
'superseded',
'unknown',
]);
const TRANSLATION_STATES = Object.freeze([
'unavailable',
'not_translated',
'machine_assisted',
'human_reviewed',
]);
function normalizeWhitespace(value) {
return String(value ?? '').replace(/\s+/g, ' ').trim();
}
function sha256(value) {
return createHash('sha256').update(String(value)).digest('hex');
}
function known(value) {
return { status: 'known', value };
}
function unknown(reason) {
return { status: 'unknown', reason };
}
function notApplicable(reason) {
return { status: 'not_applicable', reason };
}
function matchedTerms(text, terms) {
return terms.filter((term) => text.includes(term));
}
function classification(actionType, status, confidence, terms) {
return {
actionType,
status,
confidence,
matchedTerms: terms,
};
}
export function classifyPolicyDocument(title, originalText) {
const titleText = normalizeWhitespace(title);
const text = normalizeWhitespace(`${title} ${originalText}`);
const consultationTerms = matchedTerms(titleText, ['征求意见稿', '公开征求意见', '草案']);
if (consultationTerms.length > 0) {
return classification('consultation_draft', 'draft', 1, consultationTerms);
}
const lineageTerms = matchedTerms(titleText, ['修改', '修订', '更正', '纠正', '废止', '撤销', '取代', '替代']);
if (lineageTerms.length > 0) {
let status = 'amended';
if (lineageTerms.includes('更正') || lineageTerms.includes('纠正')) status = 'corrected';
else if (lineageTerms.includes('废止') || lineageTerms.includes('撤销')) status = 'repealed';
else if (lineageTerms.includes('取代') || lineageTerms.includes('替代')) status = 'superseded';
return classification(
'amendment_correction_repeal_supersession',
status,
0.98,
lineageTerms,
);
}
const enforcementTerms = matchedTerms(text, [
'行政处罚决定',
'处罚决定书',
'决定罚款',
'处以罚款',
'没收违法所得',
'责令停产',
'吊销许可证',
]);
if (enforcementTerms.length > 0) {
return classification('enforcement_penalty', 'enforced', 0.99, enforcementTerms);
}
const investigationTerms = matchedTerms(titleText, [
'立案调查',
'反倾销调查',
'反补贴调查',
'调查的初步裁定',
'启动调查',
]);
if (investigationTerms.length > 0) {
return classification('investigation', 'under_investigation', 0.98, investigationTerms);
}
const effectiveDateTerms = matchedTerms(titleText, ['调整施行日期', '延期施行', '推迟生效', '提前施行']);
if (effectiveDateTerms.length > 0) {
return classification('effective_date_change', 'amended', 0.98, effectiveDateTerms);
}
const restrictionTerms = matchedTerms(titleText, [
'禁止出口',
'限制出口',
'列入出口管制管控名单',
'行政许可',
'予以批准',
'批复',
]);
if (restrictionTerms.length > 0) {
return classification(
'administrative_approval_restriction',
'announced',
0.94,
restrictionTerms,
);
}
const finalRuleTerms = [];
if (/令〔?\d{4}〕?第?\d+号/.test(titleText)) finalRuleTerms.push('official_order_number');
for (const term of ['条例', '规定', '办法', '规章']) {
if (titleText.includes(term)) finalRuleTerms.push(term);
}
if (finalRuleTerms.length > 0) {
return classification('final_rule', 'unknown', 0.88, [...new Set(finalRuleTerms)]);
}
const announcementTerms = matchedTerms(titleText, ['公告', '通知', '指导意见', '意见', '方案', '指南']);
if (announcementTerms.length > 0) {
return classification('announcement_guidance', 'announced', 0.86, announcementTerms);
}
return classification('unknown', 'unknown', 0.25, []);
}
export function reviewedSectorLinks(text) {
const normalized = normalizeWhitespace(text);
return SECTOR_RULES
.filter((rule) => rule.pattern.test(normalized))
.map(({ id, label }) => ({ id, label, reviewState: 'reviewed' }));
}
export function reviewedEntityLinks(text) {
const normalized = normalizeWhitespace(text);
return REVIEWED_ENTITIES
.filter((entity) => normalized.includes(entity.name))
.map((entity) => ({ ...entity, reviewState: 'reviewed' }));
}
export function extractDocumentNumber(text) {
const normalized = normalizeWhitespace(text);
return normalized.match(/[\u4e00-\u9fff]{1,20}(?:令|公告|函|发)〔\d{4}〕第?\d+号/)?.[0]
?? normalized.match(/(?:令|公告)〔?\d{4}〕?第\d+号/)?.[0]
?? normalized.match(/(?:公告|令)\d{4}年第\d+号/)?.[0]
?? null;
}
function adjustedStatus(classified, effectiveDate, retrievedAt) {
if (classified.actionType !== 'final_rule') return classified.status;
if (!effectiveDate) return 'unknown';
return effectiveDate <= retrievedAt.slice(0, 10) ? 'in_force' : 'announced';
}
function lineageIdentity(input) {
return normalizeWhitespace(input.documentNumber)
|| normalizeWhitespace(input.canonicalUrl);
}
function currentMirrorIdentity(event) {
if (normalizeWhitespace(event.documentNumber)) {
return `${event.lineageId}:${event.contentHash}`;
}
return [
event.agency,
normalizeWhitespace(event.titleOriginal),
event.publicationDate,
event.contentHash,
].join(':');
}
function provenanceFor(event, extractionConfidence) {
const publisher = PUBLISHERS[event.agency];
const translationValue = {
state: event.translation.state,
...(event.translation.targetLanguage
? { targetLanguage: event.translation.targetLanguage }
: {}),
};
return {
contractVersion: 'decision-signal-provenance/v1',
signalId: event.id,
familyId: 'typed_document_event',
claims: {
publisher: known({
id: publisher.id,
name: publisher.name,
type: 'official_government',
registryReference: {
sourceName: publisher.sourceName,
sourceType: 'gov',
propagandaRisk: 'high',
},
}),
source_url: known(event.canonicalUrl),
original_reference: known({
kind: 'document',
id: event.canonicalUrl,
contentHash: event.contentHash,
}),
original_language: known(event.originalLanguage),
translation: known(translationValue),
observation_time: notApplicable('Policy documents are publication events, not observations'),
effective_time: event.effectiveDate
? known({ role: 'effective', value: event.effectiveDate, precision: 'day' })
: unknown('The official document did not expose an unambiguous effective date'),
publication_time: known({
role: 'publication',
value: event.publicationDate,
precision: 'day',
}),
retrieval_time: known({
role: 'retrieval',
value: event.retrievedAt,
precision: 'instant',
}),
revision: known({
vintageId: event.id,
sequence: event.revision.sequence,
state: event.revision.state,
}),
supersession: known({
state: event.supersession.state,
...(event.supersession.relatedEventId
? { relatedSignalId: event.supersession.relatedEventId }
: {}),
...(event.supersession.reason ? { reason: event.supersession.reason } : {}),
}),
extraction_confidence: known({
score: extractionConfidence,
method: 'official-html-json-parser/v1',
}),
classification_confidence: known({
score: event.classification.confidence,
method: 'reviewed-policy-taxonomy-rules/v1',
}),
corroboration: unknown('The official publisher is authoritative but independent corroboration was not evaluated'),
transport_freshness: known({
state: 'fresh',
assessedAt: event.retrievedAt,
lastSuccessAt: event.retrievedAt,
}),
content_freshness: known({
state: 'current',
assessedAt: event.retrievedAt,
contentAsOf: event.publicationDate,
}),
derivation: notApplicable('Typed policy events are extracted from original documents'),
},
};
}
export function normalizePolicyDocument(input) {
if (!CHINA_POLICY_AGENCIES.includes(input.agency)) {
throw new TypeError(`Unsupported China policy agency: ${String(input.agency)}`);
}
const titleOriginal = normalizeWhitespace(input.titleOriginal);
const originalText = normalizeWhitespace(input.originalText);
const canonicalUrl = new URL(input.canonicalUrl).href;
const documentNumber = normalizeWhitespace(input.documentNumber) || extractDocumentNumber(
`${titleOriginal} ${originalText}`,
);
const contentHashHex = sha256(originalText);
const lineageKey = `${input.agency}:${lineageIdentity({
documentNumber,
titleOriginal,
canonicalUrl,
})}`;
const lineageId = `china-policy-lineage:${sha256(lineageKey).slice(0, 20)}`;
const id = `china-policy:${input.agency.toLowerCase()}:${sha256(lineageKey).slice(0, 16)}:${contentHashHex.slice(0, 12)}`;
const classified = classifyPolicyDocument(titleOriginal, originalText);
const translation = input.translation ?? { state: 'not_translated' };
const event = {
id,
lineageId,
agency: input.agency,
actionType: classified.actionType,
status: adjustedStatus(classified, input.effectiveDate, input.retrievedAt),
titleOriginal,
titleTranslated: input.titleTranslated ?? null,
originalText,
originalLanguage: 'zh-CN',
translation,
canonicalUrl,
mirrorUrls: [],
documentNumber,
publicationDate: input.publicationDate,
retrievedAt: input.retrievedAt,
effectiveDate: input.effectiveDate ?? null,
sectors: reviewedSectorLinks(`${titleOriginal} ${originalText}`),
entities: reviewedEntityLinks(`${titleOriginal} ${originalText}`),
classification: {
confidence: classified.confidence,
method: 'reviewed-policy-taxonomy-rules/v1',
matchedTerms: classified.matchedTerms,
uncertain: classified.actionType === 'unknown',
},
contentHash: `sha256:${contentHashHex}`,
revision: {
state: 'original',
sequence: 1,
previousEventId: null,
},
supersession: {
state: 'current',
relatedEventId: null,
reason: null,
},
};
event.provenance = provenanceFor(event, Number.isFinite(input.extractionConfidence)
? input.extractionConfidence
: 0.95);
return event;
}
function withProvenanceLineage(event) {
return {
...event,
provenance: provenanceFor(event, event.provenance.claims.extraction_confidence.value.score),
};
}
export function reconcilePolicyEvents(currentEvents, previousEvents = []) {
const dedupedByContent = new Map();
for (const event of currentEvents) {
const key = currentMirrorIdentity(event);
const existing = dedupedByContent.get(key);
if (!existing) {
dedupedByContent.set(key, {
...event,
mirrorUrls: [...new Set(event.mirrorUrls ?? [])],
});
continue;
}
existing.mirrorUrls = [...new Set([
...existing.mirrorUrls,
event.canonicalUrl,
...(event.mirrorUrls ?? []),
])].filter((url) => url !== existing.canonicalUrl);
}
const result = [];
for (const event of dedupedByContent.values()) {
const lineageHistory = previousEvents
.filter((previous) => previous.lineageId === event.lineageId)
.sort((a, b) => (b.revision?.sequence ?? 1) - (a.revision?.sequence ?? 1));
if (lineageHistory.length === 0) {
const mirrorIdentity = currentMirrorIdentity(event);
const previousMirror = previousEvents.find(
(previous) => currentMirrorIdentity(previous) === mirrorIdentity,
);
if (previousMirror) {
result.push(...previousEvents
.filter((previous) => previous.lineageId === previousMirror.lineageId)
.map((previous) => (
previous.id === previousMirror.id
? withProvenanceLineage({
...previous,
mirrorUrls: [...new Set([
...(previous.mirrorUrls ?? []),
event.canonicalUrl,
...(event.mirrorUrls ?? []),
])].filter((url) => url !== previous.canonicalUrl),
})
: previous
)));
continue;
}
}
const previous = lineageHistory[0];
if (previous?.contentHash === event.contentHash) {
result.push(withProvenanceLineage({
...event,
id: previous.id,
canonicalUrl: previous.canonicalUrl,
mirrorUrls: [...new Set([
...(previous.mirrorUrls ?? []),
event.canonicalUrl,
...(event.mirrorUrls ?? []),
])].filter((url) => url !== previous.canonicalUrl),
revision: previous.revision,
}), ...lineageHistory.slice(1));
continue;
}
if (!previous) {
result.push(withProvenanceLineage(event));
continue;
}
const correction = /更正|纠正/.test(`${event.titleOriginal} ${event.originalText}`);
const revised = withProvenanceLineage({
...event,
revision: {
state: correction ? 'corrected' : 'revised',
sequence: (previous.revision?.sequence ?? 1) + 1,
previousEventId: previous.id,
},
});
const supersededPrevious = withProvenanceLineage({
...previous,
supersession: {
state: 'superseded',
relatedEventId: revised.id,
reason: 'A newer official document vintage was retrieved',
},
});
result.push(revised, supersededPrevious, ...lineageHistory.slice(1));
}
return result.sort((a, b) => {
const publicationDelta = Date.parse(b.publicationDate) - Date.parse(a.publicationDate);
if (publicationDelta !== 0) return publicationDelta;
return (b.revision?.sequence ?? 1) - (a.revision?.sequence ?? 1);
});
}
function isRecord(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isNonEmptyString(value) {
return typeof value === 'string' && value.trim().length > 0;
}
function isCalendarDay(value) {
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
const [year, month, day] = value.split('-').map(Number);
const date = new Date(Date.UTC(year, month - 1, day));
return date.getUTCFullYear() === year
&& date.getUTCMonth() === month - 1
&& date.getUTCDate() === day;
}
function isInstant(value) {
return typeof value === 'string'
&& /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value)
&& Number.isFinite(Date.parse(value));
}
function isOfficialUrl(value, agency) {
if (!isNonEmptyString(value)) return false;
try {
const parsed = new URL(value);
return parsed.protocol === 'https:'
&& parsed.hostname === {
CAC: 'www.cac.gov.cn',
SAMR: 'www.samr.gov.cn',
MIIT: 'www.miit.gov.cn',
MOFCOM: 'www.mofcom.gov.cn',
NDRC: 'zfxxgk.ndrc.gov.cn',
PBOC: 'www.pbc.gov.cn',
}[agency]
&& !parsed.username
&& !parsed.password;
} catch {
return false;
}
}
function isReviewedLinks(value) {
return Array.isArray(value) && value.every((link) => (
isRecord(link)
&& isNonEmptyString(link.id)
&& link.reviewState === 'reviewed'
&& (link.name === undefined || isNonEmptyString(link.name))
&& (link.label === undefined || isNonEmptyString(link.label))
));
}
function isTranslation(value) {
return isRecord(value)
&& TRANSLATION_STATES.includes(value.state)
&& (value.targetLanguage === undefined || isNonEmptyString(value.targetLanguage))
&& (value.failureReason === undefined || isNonEmptyString(value.failureReason))
&& (value.state !== 'unavailable' || isNonEmptyString(value.failureReason));
}
function isAgencyHealth(value) {
return isRecord(value)
&& ['ok', 'partial', 'error'].includes(value.status)
&& isInstant(value.retrievedAt)
&& Number.isInteger(value.documentCount)
&& value.documentCount >= 0
&& Number.isInteger(value.requestCount)
&& value.requestCount >= 1
&& Array.isArray(value.failures)
&& value.failures.every((failure) => (
isRecord(failure)
&& isNonEmptyString(failure.canonicalUrl)
&& isNonEmptyString(failure.reason)
));
}
function hasValidProvenance(value) {
if (!isRecord(value)
|| value.contractVersion !== 'decision-signal-provenance/v1'
|| value.familyId !== 'typed_document_event'
|| typeof value.signalId !== 'string'
|| !isRecord(value.claims)) {
return false;
}
if (!PROVENANCE_DIMENSIONS.every((dimension) => isRecord(value.claims[dimension]))) return false;
for (const dimension of PROVENANCE_DIMENSIONS) {
const claim = value.claims[dimension];
if (!['known', 'unknown', 'not_applicable'].includes(claim.status)) return false;
if (claim.status === 'known') {
if (!Object.prototype.hasOwnProperty.call(claim, 'value') || claim.value == null) return false;
} else if (
!isNonEmptyString(claim.reason)
|| Object.prototype.hasOwnProperty.call(claim, 'value')
) {
return false;
}
}
for (const required of [
'publisher',
'source_url',
'original_reference',
'original_language',
'translation',
'publication_time',
'retrieval_time',
'supersession',
'extraction_confidence',
'classification_confidence',
'transport_freshness',
'content_freshness',
]) {
if (value.claims[required].status !== 'known') return false;
}
return value.claims.observation_time.status === 'not_applicable'
&& value.claims.derivation.status === 'not_applicable';
}
export function validateChinaPolicyPayload(value) {
if (!isRecord(value)
|| value.schemaVersion !== 1
|| !isInstant(value.generatedAt)
|| !isRecord(value.agencies)
|| Object.keys(value.agencies).length !== CHINA_POLICY_AGENCIES.length
|| !CHINA_POLICY_AGENCIES.every((agency) => isAgencyHealth(value.agencies[agency]))
|| !Array.isArray(value.events)
|| value.events.length > 120) {
return false;
}
return value.events.every((event) => (
isRecord(event)
&& CHINA_POLICY_AGENCIES.includes(event.agency)
&& isNonEmptyString(event.id)
&& isNonEmptyString(event.lineageId)
&& ACTION_TYPES.includes(event.actionType)
&& POLICY_STATUSES.includes(event.status)
&& isNonEmptyString(event.titleOriginal)
&& (event.titleTranslated === null || isNonEmptyString(event.titleTranslated))
&& isNonEmptyString(event.originalText)
&& event.originalLanguage === 'zh-CN'
&& isTranslation(event.translation)
&& isOfficialUrl(event.canonicalUrl, event.agency)
&& Array.isArray(event.mirrorUrls)
&& event.mirrorUrls.every((url) => isOfficialUrl(url, event.agency))
&& (event.documentNumber === null || isNonEmptyString(event.documentNumber))
&& isCalendarDay(event.publicationDate)
&& isInstant(event.retrievedAt)
&& (event.effectiveDate === null || isCalendarDay(event.effectiveDate))
&& isReviewedLinks(event.sectors)
&& isReviewedLinks(event.entities)
&& isRecord(event.classification)
&& Number.isFinite(event.classification.confidence)
&& event.classification.confidence >= 0
&& event.classification.confidence <= 1
&& isNonEmptyString(event.classification.method)
&& Array.isArray(event.classification.matchedTerms)
&& event.classification.matchedTerms.every(isNonEmptyString)
&& typeof event.classification.uncertain === 'boolean'
&& /^sha256:[0-9a-f]{64}$/.test(event.contentHash)
&& isRecord(event.revision)
&& ['original', 'revised', 'corrected'].includes(event.revision.state)
&& Number.isInteger(event.revision.sequence)
&& event.revision.sequence >= 1
&& (event.revision.previousEventId === null || isNonEmptyString(event.revision.previousEventId))
&& (event.revision.sequence === 1
? event.revision.previousEventId === null
: isNonEmptyString(event.revision.previousEventId))
&& isRecord(event.supersession)
&& ['current', 'corrected', 'cancelled', 'superseded'].includes(event.supersession.state)
&& (event.supersession.relatedEventId === null || isNonEmptyString(event.supersession.relatedEventId))
&& (event.supersession.reason === null || isNonEmptyString(event.supersession.reason))
&& hasValidProvenance(event.provenance)
&& event.provenance.signalId === event.id
));
}
export const __testing__ = Object.freeze({
PUBLISHERS,
PROVENANCE_DIMENSIONS,
});