@method-ai-hq/sdk 0.0.0-stage → 0.14.3
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/LICENSE +21 -0
- package/README.md +23 -2
- package/dist/packages/contracts/src/identity.d.ts +16 -0
- package/dist/packages/contracts/src/identity.js +41 -0
- package/dist/packages/contracts/src/method-package.d.ts +71 -0
- package/dist/packages/contracts/src/method-package.js +22 -0
- package/dist/packages/method-document/src/conversion-report.d.ts +138 -0
- package/dist/packages/method-document/src/conversion-report.js +23 -0
- package/dist/packages/sdk/examples/daily-briefing/README.md +31 -0
- package/dist/packages/sdk/examples/daily-briefing/TASK.md +3 -0
- package/dist/packages/sdk/examples/daily-briefing/approved-report.md +138 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_artifacts.py +39 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_check_inputs.py +47 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_files.py +31 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_manifest.py +23 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_markdown.py +67 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_progress.py +18 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_render.py +71 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_sessions.py +37 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_times.py +158 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_validation.py +101 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_website.py +215 -0
- package/dist/packages/sdk/examples/daily-briefing/checks.json +19 -0
- package/dist/packages/sdk/examples/daily-briefing/daily-briefing.method +158 -0
- package/dist/packages/sdk/examples/daily-briefing/inputs.json +1 -0
- package/dist/packages/sdk/examples/daily-briefing/package-lock.json +10 -0
- package/dist/packages/sdk/examples/daily-briefing/package.json +4 -0
- package/dist/packages/sdk/examples/daily-briefing/pyproject.toml +8 -0
- package/dist/packages/sdk/examples/daily-briefing/reader/reader.css +35 -0
- package/dist/packages/sdk/examples/daily-briefing/reader/reader.js +42 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/context.json +1 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/documents/document-001.md +219 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/media.json +1 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/metadata/documents.json +39 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0001.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0002.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0003.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0004.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0005.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0006.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0007.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0008.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0009.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0010.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0011.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0012.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0013.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0014.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0015.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0016.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0017.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0018.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0019.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0020.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0021.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0022.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0023.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0024.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0025.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0026.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0027.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0028.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/sessions.json +1 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/timeline.jsonl +28 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/untimed.jsonl +0 -0
- package/dist/packages/sdk/examples/daily-briefing/sample-output.zip +0 -0
- package/dist/packages/sdk/examples/daily-briefing/sample-report.md +51 -0
- package/dist/packages/sdk/examples/daily-briefing/uv.lock +128 -0
- package/dist/packages/sdk/examples/daily-briefing/vendor/marked-LICENSE.md +44 -0
- package/dist/packages/sdk/examples/daily-briefing/vendor/marked.mjs +77 -0
- package/dist/packages/sdk/examples/files.json +126 -0
- package/dist/packages/sdk/examples/message-routing/README.md +23 -0
- package/dist/packages/sdk/examples/message-routing/TASK.md +5 -0
- package/dist/packages/sdk/examples/message-routing/choose-destination.mjs +10 -0
- package/dist/packages/sdk/examples/message-routing/create-ticket.mjs +15 -0
- package/dist/packages/sdk/examples/message-routing/inputs.json +1 -0
- package/dist/packages/sdk/examples/message-routing/message-routing.method +45 -0
- package/dist/packages/sdk/examples/message-routing/result.fixture.json +6 -0
- package/dist/packages/sdk/examples/message-routing/routing.mjs +29 -0
- package/dist/packages/sdk/examples/message-routing/routing.test.mjs +27 -0
- package/dist/packages/sdk/examples/message-routing/ticket.method +88 -0
- package/dist/packages/sdk/examples/message-routing/ticket.test.mjs +64 -0
- package/dist/packages/sdk/examples/notes-summary/README.md +23 -0
- package/dist/packages/sdk/examples/notes-summary/TASK.md +3 -0
- package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/case.json +36 -0
- package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/examples.json +16 -0
- package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/files/0 +10 -0
- package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/recording.json +195 -0
- package/dist/packages/sdk/examples/notes-summary/notes/2026-10-05-standup.md +4 -0
- package/dist/packages/sdk/examples/notes-summary/notes/2026-10-07-planning.md +3 -0
- package/dist/packages/sdk/examples/notes-summary/notes-summary.method +54 -0
- package/dist/packages/sdk/examples/notes-summary/out/.keep +0 -0
- package/dist/packages/sdk/examples/notes-summary/read-notes.mjs +4 -0
- package/dist/packages/sdk/examples/notes-summary/save.mjs +7 -0
- package/dist/packages/sdk/examples/outbound-management/README.md +119 -0
- package/dist/packages/sdk/examples/outbound-management/TASK.md +5 -0
- package/dist/packages/sdk/examples/outbound-management/check_plan.py +16 -0
- package/dist/packages/sdk/examples/outbound-management/check_sources.py +14 -0
- package/dist/packages/sdk/examples/outbound-management/crm.py +153 -0
- package/dist/packages/sdk/examples/outbound-management/crm_test.py +141 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/README.md +15 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/crm.json +54 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/day-1/messages.json +29 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/day-2/messages.json +38 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/observations.json +12 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/plan.json +69 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/receipt.json +1 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/state.json +99 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/tasks.md +16 -0
- package/dist/packages/sdk/examples/outbound-management/inputs.json +4 -0
- package/dist/packages/sdk/examples/outbound-management/outbound.method +277 -0
- package/dist/packages/sdk/examples/outbound-management/read_crm.py +4 -0
- package/dist/packages/sdk/examples/outbound-management/save_day.py +4 -0
- package/dist/packages/sdk/examples/outbound-management/starter/crm.json +11 -0
- package/dist/packages/sdk/examples/social-briefing/README.md +41 -0
- package/dist/packages/sdk/examples/social-briefing/TASK.md +5 -0
- package/dist/packages/sdk/examples/social-briefing/inputs.json +5 -0
- package/dist/packages/sdk/examples/social-briefing/result.fixture.json +14 -0
- package/dist/packages/sdk/examples/social-briefing/social-briefing.method +110 -0
- package/dist/packages/sdk/examples/support-triage/README.md +77 -0
- package/dist/packages/sdk/examples/support-triage/TASK.md +3 -0
- package/dist/packages/sdk/examples/support-triage/cases/double-charge-to-billing/case.json +40 -0
- package/dist/packages/sdk/examples/support-triage/cases/double-charge-to-billing/recording.json +460 -0
- package/dist/packages/sdk/examples/support-triage/fake-helpdesk.mjs +38 -0
- package/dist/packages/sdk/examples/support-triage/helpdesk.mjs +33 -0
- package/dist/packages/sdk/examples/support-triage/inputs.json +1 -0
- package/dist/packages/sdk/examples/support-triage/read-ticket.mjs +8 -0
- package/dist/packages/sdk/examples/support-triage/route.mjs +4 -0
- package/dist/packages/sdk/examples/support-triage/sample-tickets.json +5 -0
- package/dist/packages/sdk/examples/support-triage/set-ticket.mjs +9 -0
- package/dist/packages/sdk/examples/support-triage/support-triage.method +165 -0
- package/dist/packages/sdk/examples/support-triage/triage.mjs +38 -0
- package/dist/packages/sdk/examples/support-triage/triage.test.mjs +68 -0
- package/dist/packages/sdk/observers/mail.delivery/fetch.mjs +87 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/bounce-by-recipient.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/bounce.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/delayed.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/delivered.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/empty.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/other-message.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/judge.mjs +13 -0
- package/dist/packages/sdk/observers/mail.delivery/observer.json +14 -0
- package/dist/packages/sdk/package.json +232 -0
- package/dist/packages/sdk/src/authoring-example.d.ts +91 -0
- package/dist/packages/sdk/src/authoring-example.js +50 -0
- package/dist/packages/sdk/src/authoring-instructions.d.ts +20 -0
- package/dist/packages/sdk/src/authoring-instructions.js +151 -0
- package/dist/packages/sdk/src/authoring-prompt.d.ts +2 -0
- package/dist/packages/sdk/src/authoring-prompt.js +8 -0
- package/dist/packages/sdk/src/authoring.d.ts +10 -0
- package/dist/packages/sdk/src/authoring.js +287 -0
- package/dist/packages/sdk/src/bindings.d.ts +4 -0
- package/dist/packages/sdk/src/bindings.js +124 -0
- package/dist/packages/sdk/src/bridge.d.ts +2 -0
- package/dist/packages/sdk/src/bridge.js +23 -0
- package/dist/packages/sdk/src/browser-runtime/pyproject.toml +5 -0
- package/dist/packages/sdk/src/browser-runtime/tools.json +189 -0
- package/dist/packages/sdk/src/browser-runtime/uv.lock +3894 -0
- package/dist/packages/sdk/src/browser-service.py +127 -0
- package/dist/packages/sdk/src/browser.d.ts +38 -0
- package/dist/packages/sdk/src/browser.js +203 -0
- package/dist/packages/sdk/src/capabilities.d.ts +13 -0
- package/dist/packages/sdk/src/capabilities.js +68 -0
- package/dist/packages/sdk/src/classification-client.d.ts +4 -0
- package/dist/packages/sdk/src/classification-client.js +27 -0
- package/dist/packages/sdk/src/cloud.d.ts +23 -0
- package/dist/packages/sdk/src/cloud.js +43 -0
- package/dist/packages/sdk/src/computer-settings.d.ts +28 -0
- package/dist/packages/sdk/src/computer-settings.js +57 -0
- package/dist/packages/sdk/src/connect.d.ts +47 -0
- package/dist/packages/sdk/src/connect.js +203 -0
- package/dist/packages/sdk/src/current-inspection.d.ts +3 -0
- package/dist/packages/sdk/src/current-inspection.js +146 -0
- package/dist/packages/sdk/src/current-runtime.d.ts +14 -0
- package/dist/packages/sdk/src/current-runtime.js +167 -0
- package/dist/packages/sdk/src/doctor.d.ts +17 -0
- package/dist/packages/sdk/src/doctor.js +68 -0
- package/dist/packages/sdk/src/executable.d.ts +2 -0
- package/dist/packages/sdk/src/executable.js +22 -0
- package/dist/packages/sdk/src/explain-effects.d.ts +32 -0
- package/dist/packages/sdk/src/explain-effects.js +192 -0
- package/dist/packages/sdk/src/explain.d.ts +115 -0
- package/dist/packages/sdk/src/explain.js +441 -0
- package/dist/packages/sdk/src/files.d.ts +9 -0
- package/dist/packages/sdk/src/files.js +50 -0
- package/dist/packages/sdk/src/guard-keys.d.ts +5 -0
- package/dist/packages/sdk/src/guard-keys.js +42 -0
- package/dist/packages/sdk/src/hosted-models.d.ts +49 -0
- package/dist/packages/sdk/src/hosted-models.js +81 -0
- package/dist/packages/sdk/src/improve.d.ts +55 -0
- package/dist/packages/sdk/src/improve.js +159 -0
- package/dist/packages/sdk/src/index.d.ts +13 -0
- package/dist/packages/sdk/src/index.js +13 -0
- package/dist/packages/sdk/src/inspect.d.ts +6 -0
- package/dist/packages/sdk/src/inspect.js +17 -0
- package/dist/packages/sdk/src/link.d.ts +12 -0
- package/dist/packages/sdk/src/link.js +53 -0
- package/dist/packages/sdk/src/local-cli.d.ts +24 -0
- package/dist/packages/sdk/src/local-cli.js +146 -0
- package/dist/packages/sdk/src/local-setup.d.ts +15 -0
- package/dist/packages/sdk/src/local-setup.js +42 -0
- package/dist/packages/sdk/src/method-client.d.ts +30 -0
- package/dist/packages/sdk/src/method-client.js +250 -0
- package/dist/packages/sdk/src/method-files.d.ts +29 -0
- package/dist/packages/sdk/src/method-files.js +93 -0
- package/dist/packages/sdk/src/method-help.d.ts +19 -0
- package/dist/packages/sdk/src/method-help.js +404 -0
- package/dist/packages/sdk/src/method-issues.d.ts +86 -0
- package/dist/packages/sdk/src/method-issues.js +192 -0
- package/dist/packages/sdk/src/method-sync.d.ts +46 -0
- package/dist/packages/sdk/src/method-sync.js +293 -0
- package/dist/packages/sdk/src/method.d.ts +5 -0
- package/dist/packages/sdk/src/method.js +359 -0
- package/dist/packages/sdk/src/model-checks.d.ts +96 -0
- package/dist/packages/sdk/src/model-checks.js +181 -0
- package/dist/packages/sdk/src/outbox.d.ts +120 -0
- package/dist/packages/sdk/src/outbox.js +297 -0
- package/dist/packages/sdk/src/own-model-key.d.ts +22 -0
- package/dist/packages/sdk/src/own-model-key.js +76 -0
- package/dist/packages/sdk/src/prepare.d.ts +24 -0
- package/dist/packages/sdk/src/prepare.js +245 -0
- package/dist/packages/sdk/src/process.d.ts +15 -0
- package/dist/packages/sdk/src/process.js +107 -0
- package/dist/packages/sdk/src/production.d.ts +182 -0
- package/dist/packages/sdk/src/production.js +284 -0
- package/dist/packages/sdk/src/progress.d.ts +12 -0
- package/dist/packages/sdk/src/progress.js +48 -0
- package/dist/packages/sdk/src/proposals.d.ts +73 -0
- package/dist/packages/sdk/src/proposals.js +284 -0
- package/dist/packages/sdk/src/quality.d.ts +82 -0
- package/dist/packages/sdk/src/quality.js +294 -0
- package/dist/packages/sdk/src/result-files.d.ts +5 -0
- package/dist/packages/sdk/src/result-files.js +149 -0
- package/dist/packages/sdk/src/result-transfer.d.ts +21 -0
- package/dist/packages/sdk/src/result-transfer.js +37 -0
- package/dist/packages/sdk/src/run-data.d.ts +11 -0
- package/dist/packages/sdk/src/run-data.js +80 -0
- package/dist/packages/sdk/src/run-method.d.ts +2 -0
- package/dist/packages/sdk/src/run-method.js +2 -0
- package/dist/packages/sdk/src/run-saved.d.ts +5 -0
- package/dist/packages/sdk/src/run-saved.js +138 -0
- package/dist/packages/sdk/src/run-worker.d.ts +6 -0
- package/dist/packages/sdk/src/run-worker.js +131 -0
- package/dist/packages/sdk/src/secrets.d.ts +37 -0
- package/dist/packages/sdk/src/secrets.js +196 -0
- package/dist/packages/sdk/src/update.d.ts +5 -0
- package/dist/packages/sdk/src/update.js +39 -0
- package/dist/packages/sdk/src/versions.d.ts +58 -0
- package/dist/packages/sdk/src/versions.js +144 -0
- package/dist/packages/sdk/src/worker.d.ts +120 -0
- package/dist/packages/sdk/src/worker.js +443 -0
- package/dist/packages/workflow-language/src/execution.d.ts +1 -0
- package/dist/packages/workflow-language/src/execution.js +1 -0
- package/dist/packages/workflow-language/src/inspection.d.ts +990 -0
- package/dist/packages/workflow-language/src/inspection.js +130 -0
- package/dist/packages/workflow-language/src/result-files.d.ts +160 -0
- package/dist/packages/workflow-language/src/result-files.js +67 -0
- package/dist/packages/workflow-language/src/schema.d.ts +207 -0
- package/dist/packages/workflow-language/src/schema.js +56 -0
- package/dist/packages/workflow-language/src/validate.d.ts +11 -0
- package/dist/packages/workflow-language/src/validate.js +77 -0
- package/package.json +230 -4
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Describe only the generated website and its explicit supporting outputs."""
|
|
2
|
+
import mimetypes
|
|
3
|
+
from pathlib import PurePosixPath
|
|
4
|
+
from briefing_artifacts import digest, local, put
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def website_manifest(destination, inventory, title, extras):
|
|
8
|
+
destination = local(destination)
|
|
9
|
+
files = []
|
|
10
|
+
for relative, expected in inventory.items():
|
|
11
|
+
path = PurePosixPath(relative)
|
|
12
|
+
if path.is_absolute() or '..' in path.parts:
|
|
13
|
+
raise ValueError('Invalid website output path')
|
|
14
|
+
files.append({'path': 'website/' + relative, 'sha256': expected,
|
|
15
|
+
'media_type': mimetypes.guess_type(relative)[0] or 'application/octet-stream'})
|
|
16
|
+
for name in extras:
|
|
17
|
+
if PurePosixPath(name).name != name:
|
|
18
|
+
raise ValueError('Invalid supporting output path')
|
|
19
|
+
files.append({'path': name, 'sha256': digest(destination / name),
|
|
20
|
+
'media_type': mimetypes.guess_type(name)[0] or 'application/octet-stream'})
|
|
21
|
+
return put(destination / 'website-result.json', {
|
|
22
|
+
'schema': 'method-website/1', 'title': title, 'entrypoint': 'website/index.html', 'files': files,
|
|
23
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Read ordinary Markdown and collect source links without prescribing prose."""
|
|
2
|
+
import datetime as dt
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
from zoneinfo import ZoneInfo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse(text, day, marked=None, supporting=()):
|
|
12
|
+
date = dt.date.fromisoformat(day['day'])
|
|
13
|
+
ZoneInfo(day['timezone'])
|
|
14
|
+
supplements = list(supporting)
|
|
15
|
+
module = Path(marked or os.environ.get('DAILY_BRIEFING_MARKED') or Path(__file__).parent/'vendor/marked.mjs').resolve()
|
|
16
|
+
code = r'''
|
|
17
|
+
import {marked} from MARKED;
|
|
18
|
+
let chunks=[];for await(const c of process.stdin)chunks.push(c);
|
|
19
|
+
const text=Buffer.concat(chunks).toString();
|
|
20
|
+
const escape=s=>s.replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"');
|
|
21
|
+
const citations=[],photos=[],local_files=[];
|
|
22
|
+
const target=(href,image=false)=>{
|
|
23
|
+
if (/^(https?:|#)/.test(href)) return;
|
|
24
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith('/')) throw Error(`Unsupported file target: ${href}`);
|
|
25
|
+
const path=decodeURIComponent(href.split(/[?#]/)[0]);
|
|
26
|
+
if (path.split(/[\\/]/).some(x=>x==='..'||x==='.git'||x.startsWith('.env'))) throw Error(`Unsafe file target: ${href}`);
|
|
27
|
+
if(path)local_files.push({path,image});
|
|
28
|
+
};
|
|
29
|
+
marked.use({renderer:{
|
|
30
|
+
html(token){return token.text.startsWith('<!--')?'':escape(token.text)},
|
|
31
|
+
link(token){
|
|
32
|
+
const label=this.parser.parseInline(token.tokens), href=token.href;
|
|
33
|
+
if(href.startsWith('source:')){const index=citations.length;citations.push({target:href.slice(7),label});return `<!--briefing-citation-${index}-->`}
|
|
34
|
+
target(href); return `<a href="${escape(href)}" rel="noopener noreferrer">${label}</a>`;
|
|
35
|
+
},
|
|
36
|
+
image(token){
|
|
37
|
+
if(token.href.startsWith('source:')){const index=photos.length;photos.push({id:token.href.slice(7),caption:token.text});return `<!--briefing-photo-${index}-->`}
|
|
38
|
+
target(token.href,true); return `<img src="${escape(token.href)}" alt="${escape(token.text||'')}" loading="lazy">`;
|
|
39
|
+
}
|
|
40
|
+
}});
|
|
41
|
+
const tokens=marked.lexer(text),blocks=[];
|
|
42
|
+
for(const token of tokens){
|
|
43
|
+
if(token.type==='space')continue;
|
|
44
|
+
const one=[token];one.links=tokens.links;
|
|
45
|
+
const session=token.type==='paragraph' && token.tokens?.length===1 && token.tokens[0].type==='link' && /^sessions\.html#.+$/.test(token.tokens[0].href) ? token.tokens[0].href.split('#')[1] : undefined;
|
|
46
|
+
blocks.push({type:token.type,depth:token.depth,text:token.text||'',html:marked.parser(one),...(session?{session}: {})});
|
|
47
|
+
}
|
|
48
|
+
process.stdout.write(JSON.stringify({blocks,citations,photos,local_files}));
|
|
49
|
+
'''.replace('MARKED', json.dumps(module.as_uri()))
|
|
50
|
+
document = json.loads(subprocess.run(['node', '--input-type=module', '-e', code], input=text, text=True, capture_output=True, check=True).stdout)
|
|
51
|
+
ranges = {}
|
|
52
|
+
for citation in document['citations']:
|
|
53
|
+
ids, sep, selection = citation.pop('target').partition('#L')
|
|
54
|
+
citation['citations'] = ids.split(',')
|
|
55
|
+
if not all(re.fullmatch(r'[A-Za-z0-9_-]+', cid) for cid in citation['citations']):
|
|
56
|
+
raise ValueError('Invalid source record ID')
|
|
57
|
+
if sep:
|
|
58
|
+
match = re.fullmatch(r'(\d+)-L(\d+)', selection)
|
|
59
|
+
if not match or len(citation['citations']) != 1:
|
|
60
|
+
raise ValueError('A passage needs one source and #Lfirst-Llast')
|
|
61
|
+
bounds = [int(match[1]), int(match[2])]
|
|
62
|
+
key = 'lines-' + '-'.join(map(str, bounds))
|
|
63
|
+
ranges.setdefault(ids, {})[key] = bounds
|
|
64
|
+
citation['source_range'] = key
|
|
65
|
+
heading = next((b['text'] for b in document['blocks'] if b['type']=='heading' and b.get('depth')==1), date.strftime('%A, %B %d').replace(' 0',' '))
|
|
66
|
+
return {**document, 'day': date.isoformat(), 'timezone': day['timezone'], 'heading': heading,
|
|
67
|
+
'title': heading, 'source_ranges': ranges, 'supplements': supplements}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Report public work milestones without changing the final JSON result."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def progress(message, completed=None, total=None, unit=None):
|
|
7
|
+
fd = os.environ.get('METHOD_PROGRESS_FD')
|
|
8
|
+
if fd is None:
|
|
9
|
+
return
|
|
10
|
+
update = {'message': message}
|
|
11
|
+
if completed is not None and total is not None:
|
|
12
|
+
update.update(completed=completed, total=total)
|
|
13
|
+
if unit:
|
|
14
|
+
update['unit'] = unit
|
|
15
|
+
try:
|
|
16
|
+
os.write(int(fd), (json.dumps(update) + '\n').encode())
|
|
17
|
+
except (OSError, ValueError):
|
|
18
|
+
pass
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Render, check, and save a briefing once. Inputs remain read-only."""
|
|
2
|
+
import contextlib
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
|
|
10
|
+
from briefing_artifacts import digest, local, put, source_digest, tree
|
|
11
|
+
from briefing_files import folder
|
|
12
|
+
from briefing_manifest import website_manifest
|
|
13
|
+
from briefing_validation import supporting_files, check_website
|
|
14
|
+
from briefing_markdown import parse
|
|
15
|
+
from briefing_website import build
|
|
16
|
+
from briefing_progress import progress
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def render_and_save(args):
|
|
20
|
+
day, draft = args['selected_day'], args['draft']
|
|
21
|
+
supporting, files = supporting_files(args)
|
|
22
|
+
proposal = parse(draft, day, supporting=supporting)
|
|
23
|
+
progress('Rendering the briefing and its sources.')
|
|
24
|
+
temporary = Path(tempfile.mkdtemp(prefix='briefing-', dir=local('.')))
|
|
25
|
+
try:
|
|
26
|
+
(temporary/'briefing.md').write_text(draft)
|
|
27
|
+
for name, path in files.items(): shutil.copyfile(path, temporary/name)
|
|
28
|
+
website = temporary/'website'
|
|
29
|
+
with contextlib.redirect_stdout(sys.stderr):
|
|
30
|
+
build(folder('selected_day'), temporary/'briefing.md', website,
|
|
31
|
+
Path(__file__).parent/'vendor/marked.mjs', day, proposal)
|
|
32
|
+
for target in proposal['local_files']:
|
|
33
|
+
relative = target['path']
|
|
34
|
+
destination = (website/relative).resolve()
|
|
35
|
+
if not destination.is_relative_to(website.resolve()):
|
|
36
|
+
raise ValueError('File target leaves the website: ' + relative)
|
|
37
|
+
if destination.is_file(): continue
|
|
38
|
+
source = files.get(relative)
|
|
39
|
+
if source is None:
|
|
40
|
+
source = (folder('selected_day')/relative).resolve()
|
|
41
|
+
if not source.is_relative_to(folder('selected_day').resolve()):
|
|
42
|
+
raise ValueError('File target leaves the prepared folder: ' + relative)
|
|
43
|
+
if not source.is_file(): raise ValueError('Missing linked file: ' + relative)
|
|
44
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
shutil.copyfile(source, destination)
|
|
46
|
+
check_website(website)
|
|
47
|
+
if source_digest() != day['source_digest']:
|
|
48
|
+
raise ValueError('Prepared sources changed during the run')
|
|
49
|
+
inventory = tree(website)
|
|
50
|
+
extras = ['briefing.md', *files]
|
|
51
|
+
signature = hashlib.sha256(json.dumps({'website': inventory,
|
|
52
|
+
'files': {name:digest(temporary/name) for name in extras}}, sort_keys=True).encode()).hexdigest()
|
|
53
|
+
destination = local('saved/'+signature)
|
|
54
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
if destination.exists():
|
|
56
|
+
if tree(destination/'website') != inventory or any(digest(destination/name) != digest(temporary/name) for name in extras):
|
|
57
|
+
raise ValueError('Existing saved output changed')
|
|
58
|
+
else:
|
|
59
|
+
temporary.rename(destination)
|
|
60
|
+
published = website_manifest(destination, inventory, 'Briefing for '+day['day'], extras)
|
|
61
|
+
receipt = put(destination/'briefing.json', {'day':day['day'], 'timezone':day['timezone'],
|
|
62
|
+
'website':str((destination/'website/index.html').relative_to(local('.'))),
|
|
63
|
+
'source_digest':day['source_digest'], 'supporting_files':list(files)})
|
|
64
|
+
progress('Briefing saved. Website links are valid.')
|
|
65
|
+
return {'saved_briefing':receipt, 'published_website':published}
|
|
66
|
+
finally:
|
|
67
|
+
if temporary.exists(): shutil.rmtree(temporary)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == '__main__':
|
|
71
|
+
print(json.dumps(render_and_save(json.load(sys.stdin)), ensure_ascii=False))
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Group saved social visits for the website. This is not a time estimate."""
|
|
2
|
+
import datetime as dt
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def stamp(value):return dt.datetime.fromisoformat(value)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DEFAULT_DISPLAY_GAP_SECONDS = 300
|
|
11
|
+
|
|
12
|
+
def social_sessions(prepared, gap_seconds=DEFAULT_DISPLAY_GAP_SECONDS):
|
|
13
|
+
if gap_seconds <= 0: raise ValueError('The display gap must be positive')
|
|
14
|
+
if not (prepared/'sessions.json').exists(): return []
|
|
15
|
+
entries={r['entry_id']:r for r in map(json.loads,(prepared/'timeline.jsonl').read_text().splitlines())}
|
|
16
|
+
groups=[];source_sessions=json.loads((prepared/'sessions.json').read_text())
|
|
17
|
+
for session in source_sessions:
|
|
18
|
+
for social in session['social']:
|
|
19
|
+
segments=[]
|
|
20
|
+
for eid in social['event_ids']:
|
|
21
|
+
row=entries[eid]
|
|
22
|
+
if not segments or (stamp(row['time'])-stamp(segments[-1][-1]['time'])).total_seconds()>gap_seconds:segments.append([])
|
|
23
|
+
segments[-1].append(row)
|
|
24
|
+
for segment in segments:
|
|
25
|
+
sid='social-'+hashlib.sha256((session['session_id']+'|'+social['platform']+'|'+segment[0]['entry_id']).encode()).hexdigest()[:12]
|
|
26
|
+
start,end=segment[0]['time'],segment[-1]['time']
|
|
27
|
+
full=[entries[eid] for eid in session['event_ids'] if stamp(start)<=stamp(entries[eid]['time'])<=stamp(end)]
|
|
28
|
+
groups.append({'id':sid,'parent':session['session_id'],'platform':social['platform'],'stream':session['stream'],'start':start,'end':end,'social':segment,'full':full})
|
|
29
|
+
# Keep isolated uncertain Takeout records as separate rows in the overlapping view.
|
|
30
|
+
attached=set()
|
|
31
|
+
for g in groups:
|
|
32
|
+
if not g['stream'].startswith('takeout:') or len(g['social'])!=1:continue
|
|
33
|
+
candidates=[other for other in groups if other['stream'].startswith('browser:') and other['platform']==g['platform'] and stamp(other['start'])<=stamp(g['start'])<=stamp(other['end'])]
|
|
34
|
+
if len(candidates)==1:
|
|
35
|
+
candidates[0]['full']+=g['full'];attached.add(g['id'])
|
|
36
|
+
groups=[g for g in groups if g['id'] not in attached]
|
|
37
|
+
return sorted(groups,key=lambda g:(stamp(g["start"]),g["id"]))
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Calculate durations from explicit source selections. No model calls or prose writing."""
|
|
2
|
+
import datetime as dt
|
|
3
|
+
import itertools
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import sys
|
|
7
|
+
from zoneinfo import ZoneInfo
|
|
8
|
+
|
|
9
|
+
from briefing_files import folder
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def number(value):
|
|
13
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value < 0:
|
|
14
|
+
raise ValueError('Expected a finite, nonnegative number')
|
|
15
|
+
return value
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def stamp(value):
|
|
19
|
+
value = dt.datetime.fromisoformat(value)
|
|
20
|
+
if value.utcoffset() is None:
|
|
21
|
+
raise ValueError('An estimate needs a timestamp with a saved clock offset')
|
|
22
|
+
return value.astimezone(dt.timezone.utc)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def union(ranges):
|
|
26
|
+
merged = []
|
|
27
|
+
for start, end in sorted(ranges):
|
|
28
|
+
if end <= start:
|
|
29
|
+
continue
|
|
30
|
+
if merged and start <= merged[-1][1]:
|
|
31
|
+
merged[-1] = (merged[-1][0], max(end, merged[-1][1]))
|
|
32
|
+
else:
|
|
33
|
+
merged.append((start, end))
|
|
34
|
+
return merged
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def minutes(ranges):
|
|
38
|
+
return sum((end-start).total_seconds()/60 for start, end in union(ranges))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def clusters(points, gap):
|
|
42
|
+
groups = []
|
|
43
|
+
for point in sorted(set(points)):
|
|
44
|
+
if not groups or (point-groups[-1][-1]).total_seconds() > gap*60:
|
|
45
|
+
groups.append([])
|
|
46
|
+
groups[-1].append(point)
|
|
47
|
+
return [(group[0], group[-1]) for group in groups]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def calculate(plan, prepared, day, timezone):
|
|
51
|
+
zone = ZoneInfo(timezone)
|
|
52
|
+
date = dt.date.fromisoformat(day)
|
|
53
|
+
lower = dt.datetime.combine(date, dt.time(), zone).astimezone(dt.timezone.utc)
|
|
54
|
+
upper = dt.datetime.combine(date+dt.timedelta(days=1), dt.time(), zone).astimezone(dt.timezone.utc)
|
|
55
|
+
records = {}
|
|
56
|
+
|
|
57
|
+
def record(cid):
|
|
58
|
+
if not isinstance(cid, str) or not cid or any(c not in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_' for c in cid):
|
|
59
|
+
raise ValueError('Invalid record ID')
|
|
60
|
+
path = (prepared/'records'/f'{cid}.json').resolve(strict=True)
|
|
61
|
+
if not path.is_relative_to(prepared.resolve()):
|
|
62
|
+
raise ValueError('Record is outside prepared folder')
|
|
63
|
+
if cid not in records:
|
|
64
|
+
records[cid] = json.loads(path.read_text())['prepared']
|
|
65
|
+
return records[cid]
|
|
66
|
+
|
|
67
|
+
def value(ref):
|
|
68
|
+
obj = record(ref['record_id'])
|
|
69
|
+
for key in ref['field'].split('.'):
|
|
70
|
+
obj = obj[key]
|
|
71
|
+
return obj
|
|
72
|
+
|
|
73
|
+
def clipped(ranges):
|
|
74
|
+
result = []
|
|
75
|
+
for start, end in ranges:
|
|
76
|
+
if end < start:
|
|
77
|
+
raise ValueError('An interval ends before it starts')
|
|
78
|
+
start, end = max(start, lower), min(end, upper)
|
|
79
|
+
if end >= start:
|
|
80
|
+
result.append((start, end))
|
|
81
|
+
return union(result)
|
|
82
|
+
|
|
83
|
+
results, all_ranges, ids = [], {}, set()
|
|
84
|
+
for activity in plan['activities']:
|
|
85
|
+
aid = activity['id']
|
|
86
|
+
if aid in ids:
|
|
87
|
+
raise ValueError('Repeated activity ID')
|
|
88
|
+
ids.add(aid)
|
|
89
|
+
mode = activity['mode']
|
|
90
|
+
sources, ranges, extra = set(), [], {}
|
|
91
|
+
# Reasons and exclusions are agent decisions. The script retains them unchanged.
|
|
92
|
+
for exclusion in activity.get('excluded', []):
|
|
93
|
+
for cid in exclusion['record_ids']:
|
|
94
|
+
record(cid)
|
|
95
|
+
if mode == 'points':
|
|
96
|
+
gap = number(activity['gap_minutes'])
|
|
97
|
+
points = []
|
|
98
|
+
for ref in activity['points']:
|
|
99
|
+
point = stamp(value(ref))
|
|
100
|
+
if not lower <= point < upper:
|
|
101
|
+
raise ValueError('Point is outside the selected day')
|
|
102
|
+
sources.add(ref['record_id']); points.append(point)
|
|
103
|
+
ranges = clusters(points, gap)
|
|
104
|
+
extra['gap_sensitivity_minutes'] = {str(g): minutes(clusters(points, number(g))) for g in activity.get('compare_gaps', [])}
|
|
105
|
+
elif mode == 'intervals':
|
|
106
|
+
for interval in activity['intervals']:
|
|
107
|
+
ranges.append((stamp(value(interval['start'])), stamp(value(interval['end']))))
|
|
108
|
+
sources.update([interval['start']['record_id'], interval['end']['record_id']])
|
|
109
|
+
elif mode == 'durations':
|
|
110
|
+
divisor = {'milliseconds': 60000, 'seconds': 60, 'minutes': 1}[activity['unit']]
|
|
111
|
+
refs = activity['durations']
|
|
112
|
+
keys = [(r['record_id'], r['field']) for r in refs]
|
|
113
|
+
if len(set(keys)) != len(keys):
|
|
114
|
+
raise ValueError('Repeated duration source')
|
|
115
|
+
total = sum(number(value(ref)) for ref in refs)/divisor
|
|
116
|
+
sources.update(ref['record_id'] for ref in refs)
|
|
117
|
+
else:
|
|
118
|
+
raise ValueError('Unknown calculation mode')
|
|
119
|
+
ranges = clipped(ranges)
|
|
120
|
+
if mode != 'durations':
|
|
121
|
+
total = minutes(ranges)
|
|
122
|
+
all_ranges[aid] = ranges
|
|
123
|
+
results.append({'id': aid, 'label': activity['label'], 'minutes': total,
|
|
124
|
+
'ranges': [{'start': a.astimezone(zone).isoformat(), 'end': b.astimezone(zone).isoformat()} for a,b in ranges],
|
|
125
|
+
'source_record_ids': sorted(sources), 'reason': activity['reason'],
|
|
126
|
+
'excluded': activity.get('excluded', []), **extra})
|
|
127
|
+
overlaps = [{'activities': [a,b], 'minutes': minutes([(max(x,u),min(y,v)) for x,y in all_ranges[a] for u,v in all_ranges[b] if max(x,u)<min(y,v)])}
|
|
128
|
+
for a,b in itertools.combinations(all_ranges, 2)]
|
|
129
|
+
return {'day': day, 'timezone': timezone, 'activities': results, 'overlaps': overlaps,
|
|
130
|
+
'not_estimated': plan.get('not_estimated', [])}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def explanation(result):
|
|
134
|
+
"""Display the saved arithmetic and the writer's reasons without changing them."""
|
|
135
|
+
lines=['# Time estimates', '', 'Session spans and recorded durations can overlap. They do not measure continuous attention.', '']
|
|
136
|
+
for activity in result['activities']:
|
|
137
|
+
lines += ['## '+activity['label'], '', f"{activity['minutes']:.1f} minutes. "+activity['reason'], '']
|
|
138
|
+
sensitivity=activity.get('gap_sensitivity_minutes', {})
|
|
139
|
+
if sensitivity:
|
|
140
|
+
lines += ['Changing the session gap: '+', '.join(f'{gap} minutes → {value:.1f} minutes' for gap,value in sensitivity.items())+'.', '']
|
|
141
|
+
for excluded in activity.get('excluded', []):
|
|
142
|
+
lines += [excluded.get('reason', str(excluded)) if isinstance(excluded, dict) else str(excluded), '']
|
|
143
|
+
if result.get('not_estimated'):
|
|
144
|
+
lines += ['## Not estimated', '', *[(str(item.get('label') or item.get('id') or '')+': '+str(item.get('reason') or '')) if isinstance(item, dict) else str(item) for item in result['not_estimated']]]
|
|
145
|
+
return '\n'.join(lines)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def calculate_activity_times(args):
|
|
149
|
+
from briefing_artifacts import local, put
|
|
150
|
+
plan = json.loads(args['plan'])
|
|
151
|
+
result = calculate(plan, folder('selected_day'), args['day'], args['timezone'])
|
|
152
|
+
# Calculate first so an invalid request leaves the last successful files intact.
|
|
153
|
+
files = [put(local('time-plan.json'), plan), put(local('time-estimates.json'), result)]
|
|
154
|
+
return {'estimates': json.dumps(result, ensure_ascii=False), 'files': files}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == '__main__':
|
|
158
|
+
print(json.dumps(calculate_activity_times(json.load(sys.stdin)), ensure_ascii=False))
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Check draft references and website links."""
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from html.parser import HTMLParser
|
|
5
|
+
from urllib.parse import unquote, urlsplit
|
|
6
|
+
|
|
7
|
+
from briefing_artifacts import digest, local
|
|
8
|
+
from briefing_times import explanation
|
|
9
|
+
from briefing_files import folder
|
|
10
|
+
from briefing_markdown import parse
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def check_draft(draft, day, supporting=()):
|
|
14
|
+
doc=parse(draft, day, supporting=supporting)
|
|
15
|
+
records={p.stem for p in (folder('selected_day')/'records').glob('*.json')}
|
|
16
|
+
supplements={s['id'] for s in doc['supplements']}
|
|
17
|
+
if len(supplements)!=len(doc['supplements']):raise ValueError('Repeated supplement ID')
|
|
18
|
+
if records & supplements:raise ValueError('A supplement uses a source record ID')
|
|
19
|
+
allowed=records|supplements
|
|
20
|
+
for citation in doc['citations']:
|
|
21
|
+
if set(citation['citations'])-allowed:raise ValueError('Unknown citation')
|
|
22
|
+
for photo in doc['photos']:
|
|
23
|
+
if photo['id'] not in records:raise ValueError('Unknown photo record')
|
|
24
|
+
for cid,ranges in doc.get('source_ranges',{}).items():
|
|
25
|
+
text = (json.loads((folder('selected_day')/'records'/f'{cid}.json').read_text())['prepared']['text']
|
|
26
|
+
if cid in records else next(s['text'] for s in doc['supplements'] if s['id']==cid))
|
|
27
|
+
for first,last in ranges.values():
|
|
28
|
+
if not 1<=first<=last<=len(text.splitlines()):raise ValueError('Passage lines are outside the saved text')
|
|
29
|
+
return doc
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Links(HTMLParser):
|
|
33
|
+
def __init__(self):super().__init__();self.ids=set();self.links=[];self.citations=[];self.entries=set()
|
|
34
|
+
def handle_starttag(self,tag,attrs):
|
|
35
|
+
a=dict(attrs)
|
|
36
|
+
if 'id' in a:self.ids.add(a['id'])
|
|
37
|
+
if 'data-entry' in a:self.entries.add(a['data-entry'])
|
|
38
|
+
for name in ('href','src'):
|
|
39
|
+
if name in a:self.links.append(a[name])
|
|
40
|
+
if 'data-ids' in a:self.citations.extend(json.loads(a['data-ids']))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def check_website(root):
|
|
44
|
+
pages={}
|
|
45
|
+
for path in root.glob('*.html'):
|
|
46
|
+
page=Links();page.feed(path.read_text());pages[path.resolve()]=page
|
|
47
|
+
for path,page in pages.items():
|
|
48
|
+
for link in page.links:
|
|
49
|
+
url=urlsplit(link)
|
|
50
|
+
if url.scheme or url.netloc:continue
|
|
51
|
+
target=(path.parent/unquote(url.path)).resolve() if url.path else path
|
|
52
|
+
if not target.is_relative_to(root.resolve()) or not target.is_file():raise ValueError('Broken local website link: '+link)
|
|
53
|
+
if url.fragment and target in pages and unquote(url.fragment) not in pages[target].ids:raise ValueError('Missing passage anchor: '+link)
|
|
54
|
+
if any(not (root/'data'/f'{cid}.json').is_file() for cid in page.citations):raise ValueError('A citation has no source view')
|
|
55
|
+
groups=json.loads((root/'display-groups.json').read_text())
|
|
56
|
+
if any(g['id'] not in pages[(root/'sessions.html').resolve()].ids for g in groups):raise ValueError('A browsing group is absent from the sessions page')
|
|
57
|
+
sessions = folder('selected_day')/'sessions.json'
|
|
58
|
+
expected={eid for session in (json.loads(sessions.read_text()) if sessions.exists() else []) for social in session['social'] for eid in social['event_ids']}
|
|
59
|
+
shown={eid for group in groups for eid in group['full']}
|
|
60
|
+
if expected-shown:raise ValueError('A saved social visit is absent from the website')
|
|
61
|
+
entries=[row for name in ['timeline.jsonl','untimed.jsonl'] for row in map(json.loads,(folder('selected_day')/name).read_text().splitlines())]
|
|
62
|
+
browser_ids={row['entry_id'] for row in entries if json.loads((folder('selected_day')/'records'/f"{row['source_record_ids'][0]}.json").read_text())['prepared']['source'] in ['browser','google_takeout']}
|
|
63
|
+
if browser_ids and browser_ids-pages[(root/'browser.html').resolve()].entries:raise ValueError('A browser visit is absent from All browser visits')
|
|
64
|
+
return len(groups)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def supporting_files(args):
|
|
68
|
+
day = args['selected_day']
|
|
69
|
+
supporting, files = [], {}
|
|
70
|
+
for ref in args.get('supporting_files', []):
|
|
71
|
+
path = local(ref['path'])
|
|
72
|
+
if digest(path) != ref['sha256']:
|
|
73
|
+
raise ValueError('A supporting file changed: ' + path.name)
|
|
74
|
+
if path.suffix not in ('.json', '.md', '.txt') or path.name in ('briefing.md', 'briefing.json', 'website-result.json') or path.name in files:
|
|
75
|
+
raise ValueError('Invalid supporting file: ' + path.name)
|
|
76
|
+
text = path.read_text()
|
|
77
|
+
if path.name == 'time-estimates.json':
|
|
78
|
+
estimates = json.loads(text)
|
|
79
|
+
if (estimates['day'], estimates['timezone']) != (day['day'], day['timezone']):
|
|
80
|
+
raise ValueError('Calculated times do not match the selected date and timezone')
|
|
81
|
+
text = explanation(estimates)
|
|
82
|
+
elif path.suffix == '.json':
|
|
83
|
+
text = '```json\n'+text+'\n```'
|
|
84
|
+
files[path.name] = path
|
|
85
|
+
supporting.append({'id': path.stem, 'title': path.stem.replace('-', ' ').capitalize(), 'text': text})
|
|
86
|
+
return supporting, files
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def check_written_briefing(args):
|
|
90
|
+
try:
|
|
91
|
+
values = {**args['inputs'], **args['outputs']}
|
|
92
|
+
supporting, files = supporting_files(values)
|
|
93
|
+
doc = check_draft(values['draft'], values['selected_day'], supporting)
|
|
94
|
+
return {'status': 'pass', 'reason': 'Source links and supporting files are valid.',
|
|
95
|
+
'evidence': [f"{len(doc['citations'])} citations checked.", f"{len(files)} supporting files checked."]}
|
|
96
|
+
except (ValueError, KeyError, OSError) as error:
|
|
97
|
+
return {'status': 'fail', 'reason': str(error), 'evidence': []}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == '__main__':
|
|
101
|
+
print(json.dumps(check_written_briefing(json.load(sys.stdin))))
|