Download config.example.py from ICSAC-Institute/editorial-system: direct link, hf CLI and curl.
- Browser
- Download file 21.3 kB
-
https://huggingface.co/spaces/ICSAC-Institute/editorial-system/resolve/main/config.example.py
- Command line
-
hf download hf://spaces/ICSAC-Institute/editorial-system/config.example.py
-
curl -L -o config.example.py https://huggingface.co/spaces/ICSAC-Institute/editorial-system/resolve/main/config.example.py
21.3 kB
| """ICSAC Editorial System β Configuration. | |
| Copy this file to config.py. Secrets are loaded from environment variables. | |
| Set them in /etc/icsac/editorial.env (loaded by systemd EnvironmentFile=). | |
| For manual runs: source /etc/icsac/editorial.env && export ZENODO_TOKEN TELEGRAM_TOKEN TELEGRAM_CHAT_ID | |
| """ | |
| import os | |
| import os as _os | |
| def _load_env_file(path: str = "/etc/icsac/editorial.env") -> None: | |
| """Self-load env file if vars not already set. Lets Python invocations | |
| work without ceremony β systemd EnvironmentFile= still wins when present. | |
| """ | |
| p = _os.path.expanduser(path) | |
| if not _os.path.isfile(p): | |
| return | |
| with open(p) as f: | |
| for line in f: | |
| line = line.strip() | |
| if not line or line.startswith("#") or "=" not in line: | |
| continue | |
| k, _, v = line.partition("=") | |
| k = k.strip() | |
| v = v.strip().strip('"').strip("'") | |
| _os.environ.setdefault(k, v) | |
| _load_env_file() | |
| ZENODO_TOKEN = os.environ.get("ZENODO_TOKEN", "") | |
| ZENODO_API = "https://zenodo.org/api" | |
| TELEGRAM_TOKEN = os.environ.get("TELEGRAM_TOKEN", "") | |
| TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "") | |
| # Optional: Telegram supergroup-thread routing. When set, editorial-system | |
| # messages pin to a specific topic so notifications can share a bot/supergroup | |
| # with other operator monitoring without crossing streams. | |
| TELEGRAM_THREAD_ID = os.environ.get("TELEGRAM_THREAD_ID", "") | |
| # Optional: Tier-3 test routing. The submission worker (when test_mode and | |
| # tier=3) and apply_decision (when applying a verdict to a test sub_id) | |
| # route curator-facing Telegram to this chat instead of TELEGRAM_CHAT_ID. | |
| # Leave unset to skip the curator Telegram in T3 entirely (panel/RQC/email | |
| # draft still run); the worker logs a single warning when this happens. | |
| TELEGRAM_TEST_CHAT_ID = os.environ.get("TELEGRAM_TEST_CHAT_ID", "") | |
| TELEGRAM_TEST_THREAD_ID = os.environ.get("TELEGRAM_TEST_THREAD_ID", "") | |
| # Optional: IMAP draft-save mode. When email_send is invoked with draft=True, | |
| # the rendered MIME message is APPENDed to Gmail's Drafts folder via IMAP | |
| # (operator manually reviews + sends from Gmail UI). Leave unset to disable | |
| # draft mode entirely. | |
| IMAP_HOST = os.environ.get("IMAP_HOST", "imap.gmail.com") | |
| IMAP_PORT = int(os.environ.get("IMAP_PORT", "993")) | |
| IMAP_USER = os.environ.get("IMAP_USER", "") | |
| IMAP_PASSWORD = os.environ.get("IMAP_PASSWORD", "") | |
| IMAP_DRAFTS_FOLDER = os.environ.get("IMAP_DRAFTS_FOLDER", "[Gmail]/Drafts") | |
| OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "") | |
| HF_TOKEN = os.environ.get("HF_TOKEN", "") | |
| # Panel slot chains. Entries are tagged with backend prefix: | |
| # "hf|<model>:<provider>" β HuggingFace Inference Providers Router | |
| # (custom provider keys live in HF settings; | |
| # billing routes through the upstream provider) | |
| # "or|<model>" β OpenRouter direct | |
| # Untagged entries fall through to OR for backward compatibility. | |
| # Consecutive OR entries are batched into a single OR call (`models` array, | |
| # max 3 per OR's cap). HF entries fire one HTTP request each because HF's | |
| # explicit provider pin does not auto-failover within the call β the panel | |
| # chain dispatcher is responsible for trying the next entry on failure. | |
| # | |
| # 2026-09-27: Groq retired Llama-3.3-70B (404), Cerebras retired Qwen3-235B | |
| # (410), and Groq's free tier caps gpt-oss-120b at 8k tokens/min -- a paper is | |
| # ~10k tokens, so it 413s on every real submission. Every slot therefore fell | |
| # to its OR :free tail, which was rate-limited upstream (429), and a submission | |
| # paused twice. Primaries now pin deepinfra (paid from HF credits, ~1.5c per | |
| # paper at list price; all four verified 200 at full paper size 2026-09-27). | |
| # One model family per slot: DeepSeek / OpenAI open-weights / Qwen / Google. | |
| # Trade-off: one paid provider, so a deepinfra outage drops every slot to its | |
| # OR :free tail at once. No other provider is enabled on the HF account. | |
| # OR :free tails pruned 2026-09-27 to what OR's catalog lists (17 models): | |
| # gpt-oss-120b, glm-4.5-air, nemotron-nano-12b, hermes-3-405b and minimax-m2.5 | |
| # :free are gone -- the new model check flagged them. Live cross-family picks | |
| # with paper-sized context: qwen3.8-27b (untested in the panel; last resort) | |
| # and the two gemma-4s. Slot 3's tail skips qwen since its primary is Qwen. | |
| # Second entry per slot (2026-09-27): a cross-family deepinfra fallback. A | |
| # reviewer output that fails the schema check is a MODEL-shaped failure; the | |
| # retry used to re-walk the same primary, then fall to the OR :free tail, which | |
| # 429'd all day -- two slots lost per pass, panel below MIN_REVIEWERS. A | |
| # different model on the same provider absorbs it. Same trade-off as 04-27: | |
| # a fallback can duplicate another slot's primary; reliability beats diversity. | |
| OPENROUTER_MODELS = [ | |
| # 2026-10-02: no entry may route a manuscript to a provider that trains on | |
| # prompts. OpenRouter lists its free nemotron routes as training ones, so | |
| # they are out. The OR free routes kept here are listed as not training; | |
| # Workers AI does not train; the HF upstreams' own terms are still to check. | |
| # 2026-09-28 (the curator): a free direct provider first wherever one exists, the | |
| # HF router second (a $0.10/month credit pool), OpenRouter :free last (a shared | |
| # pool that 429s). `oai|<provider>|<model>` entries need the provider's key in | |
| # the environment (OAI_COMPAT_PROVIDERS); without it they are skipped silently, | |
| # so the roster can be wired before the keys exist. A paper the panel cannot | |
| # staff is re-queued every batch tick (panel_retry.py) and screams after 24 h. | |
| # Slot 1: DeepSeek family. | |
| [ | |
| "oai|sambanova|DeepSeek-V3.1", | |
| # 2026-10-12: HF's deepinfra route stopped serving DeepSeek-V4-Flash and | |
| # GLM-4.7-Flash. GLM-4.7-Flash now comes from Workers AI. Workers AI's own | |
| # DeepSeek-V4-Flash is left out: on a full panel prompt it spent its whole | |
| # output on hidden reasoning, and one call cost ~2,600 of the daily neurons | |
| # the gpt-oss slot also draws on. | |
| "oai|cloudflare|@cf/zai-org/glm-4.7-flash", | |
| "or|google/gemma-4-31b-it:free", | |
| ], | |
| # Slot 2: OpenAI open-weights. Workers AI first (2026-10-02): free within | |
| # the daily allocation, metered by review._neuron_reserve, and it does not | |
| # train on customer content. | |
| [ | |
| "oai|cloudflare|@cf/openai/gpt-oss-120b", | |
| "oai|sambanova|gpt-oss-120b", | |
| "hf|openai/gpt-oss-120b:deepinfra", | |
| "hf|Qwen/Qwen3-235B-A22B-Instruct-2507:deepinfra", | |
| "or|google/gemma-4-26b-a4b-it:free", | |
| ], | |
| # Slot 3: Mistral / Qwen family (Mistral activates when MISTRAL_API_KEY exists; | |
| # medium, not large: the key's model list has no large model, 2026-10-02). | |
| [ | |
| "oai|mistral|mistral-medium-latest", | |
| "hf|Qwen/Qwen3-235B-A22B-Instruct-2507:deepinfra", | |
| "hf|google/gemma-4-31B-it:deepinfra", | |
| "or|google/gemma-4-26b-a4b-it:free", | |
| "or|google/gemma-4-31b-it:free", | |
| ], | |
| # Slot 4: Google family. Workers AI first (2026-10-12): free within the | |
| # daily allocation, no training on customer content. | |
| [ | |
| "oai|cloudflare|@cf/google/gemma-4-26b-a4b-it", | |
| "hf|google/gemma-4-31B-it:deepinfra", | |
| "or|google/gemma-4-31b-it:free", | |
| "oai|sambanova|gemma-4-31B-it", | |
| "or|google/gemma-4-26b-a4b-it:free", | |
| ], | |
| ] | |
| # One-off PAID OpenRouter run: a comma list of OpenRouter model ids | |
| # in ICSAC_OR_PAID_MODELS puts "or|<model>" (no :free) at the HEAD of each slot, | |
| # in order, and at the head of the misattribution chain, for THIS process only. | |
| # A hand-run switch, never set in a unit file or the env file. | |
| OR_PAID_MODELS = [m.strip() for m in os.environ.get("ICSAC_OR_PAID_MODELS", "").split(",") if m.strip()] | |
| if OR_PAID_MODELS: | |
| for _i, _model in enumerate(OR_PAID_MODELS[:len(OPENROUTER_MODELS)]): | |
| OPENROUTER_MODELS[_i].insert(0, f"or|{_model}") | |
| OPENROUTER_MODELS_API_URL = "https://openrouter.ai/api/v1/models" | |
| # OpenAI-compatible direct providers for `oai|<provider>|<model>` panel entries | |
| # (2026-09-28). Keys come from the environment; an entry whose key is absent is | |
| # skipped, not counted as a dead chain link. | |
| OAI_COMPAT_PROVIDERS = { | |
| # no longer free (checked 2026-10-02: a payment method and purchased credits | |
| # are required); its entries stay inert while no key is set | |
| "sambanova": {"base_url": "https://api.sambanova.ai/v1", "key_env": "SAMBANOVA_API_KEY"}, | |
| # A card-backed account (2026-10-02) with a monthly allowance that must never | |
| # be exceeded, and training on inputs switched off in the account. Every call | |
| # is metered against monthly_cap_usd (ICSAC_MISTRAL_MONTHLY_CAP_USD, default | |
| # 8) before it is sent. usd_rates are deliberately ABOVE the list price | |
| # (mistral-medium-latest = Medium 3.5, listed at $1.5 / $7.5 per million | |
| # input / output tokens), so the ledger over-counts and a price rise stays | |
| # inside the margin; a model with no rate here is refused. | |
| "mistral": { | |
| "base_url": "https://api.mistral.ai/v1", | |
| "key_env": "MISTRAL_API_KEY", | |
| "monthly_cap_usd": float(os.environ.get("ICSAC_MISTRAL_MONTHLY_CAP_USD", "8")), | |
| "usd_rates": { | |
| "mistral-medium-latest": (2.0, 10.0), | |
| }, | |
| }, | |
| # Cloudflare Workers AI (2026-10-02): a free daily allocation of 10,000 | |
| # neurons per account, and customer content is not used for training. On a | |
| # Workers Paid account usage past the allocation is billed, so every call is | |
| # metered against daily_neuron_cap before it is sent and refused when it | |
| # could cross it (review._neuron_reserve). Rates are neurons per million | |
| # input / output tokens from the Workers AI pricing page; a model with no | |
| # rate here is refused. | |
| "cloudflare": { | |
| "base_url": ("https://api.cloudflare.com/client/v4/accounts/" | |
| + os.environ.get("CLOUDFLARE_AI_ACCOUNT_ID", "") + "/ai/v1"), | |
| "key_env": "CLOUDFLARE_AI_TOKEN", | |
| "daily_neuron_cap": int(os.environ.get("ICSAC_CF_NEURON_CAP", "9000")), | |
| "neuron_rates": { | |
| "@cf/openai/gpt-oss-120b": (31818, 68182), | |
| "@cf/meta/llama-4-scout-17b-16e-instruct": (24545, 77273), | |
| "@cf/mistralai/mistral-small-3.1-24b-instruct": (31876, 50488), | |
| "@cf/google/gemma-3-12b-it": (31371, 50560), | |
| "@cf/zai-org/glm-4.7-flash": (5500, 36364), | |
| "@cf/google/gemma-4-26b-a4b-it": (9091, 27273), | |
| }, | |
| # Request overrides per model (default: max_tokens 4000, no JSON mode). | |
| # Workers AI counts a reasoning model's hidden reasoning inside | |
| # max_tokens: at 4000 both of these cut their review JSON off mid-object | |
| # on a full-length panel prompt. Without JSON mode both wrote LaTeX | |
| # backslash commands unescaped inside JSON strings, which no parser accepts. | |
| # In JSON mode Gemma spent all 16000 tokens reasoning and returned no | |
| # content, so its thinking is off. Measured 2026-10-12. | |
| "model_params": { | |
| "@cf/zai-org/glm-4.7-flash": {"max_tokens": 16000, "response_format": {"type": "json_object"}}, | |
| "@cf/google/gemma-4-26b-a4b-it": {"max_tokens": 8000, "response_format": {"type": "json_object"}, | |
| "chat_template_kwargs": {"enable_thinking": False}}, | |
| }, | |
| }, | |
| } | |
| # panel_retry.py (batch-tick step 2b): a paper the panel could not staff is | |
| # re-queued every tick and SCREAMS (Telegram + pain) after this many hours of | |
| # waiting for reviewers, counted from its first pause, never from receipt. A | |
| # paper stuck in_review this long with no result is treated as a dead worker. | |
| PANEL_SCREAM_HOURS = float(os.environ.get("ICSAC_PANEL_SCREAM_HOURS", "24")) | |
| PANEL_STUCK_HOURS = float(os.environ.get("ICSAC_PANEL_STUCK_HOURS", "3")) | |
| # after this many automatic re-queues (about five days at two ticks a day) the | |
| # paper keeps screaming but is not re-run again without a human | |
| PANEL_MAX_RETRIES = int(os.environ.get("ICSAC_PANEL_MAX_RETRIES", "10")) | |
| # papers paused before this instant are announced once and left parked | |
| PANEL_RETRY_SINCE = os.environ.get("ICSAC_PANEL_RETRY_SINCE", "2026-09-28T00:00:00Z") | |
| # ββ DOI registrar βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # "crossref": accept stages a Crossref deposit draft (crossref_deposit.stage) -- | |
| # nothing is minted until an operator runs intake/register-doi.sh | |
| # --live. ICSAC has been a Crossref member since 2026-07-24. | |
| # "zenodo": the pre-membership path (repository_deposit) -- a Zenodo draft | |
| # that mints 10.5281/zenodo.* on publish. Kept for the backfile. | |
| DOI_REGISTRAR = os.environ.get("DOI_REGISTRAR", "crossref") | |
| CROSSREF_PREFIX = os.environ.get("CROSSREF_PREFIX", "10.67697") | |
| # The "Last revised" date shown on icsacinstitute.org/terms. Written into every | |
| # submission record so we can say which Terms an author accepted. Keep in sync. | |
| TERMS_VERSION = os.environ.get("ICSAC_TERMS_VERSION", "2026-09-28") | |
| # Author objection window (days) opened at accept; the acceptance email carries | |
| # a personal /approve/ link and this deadline. register --live waits for an | |
| # approval or the window's close unless the operator overrides. | |
| OBJECTION_WINDOW_DAYS = int(os.environ.get("ICSAC_OBJECTION_WINDOW_DAYS", "7")) | |
| # Days after DOI registration before the ONE post-publication follow-up (the | |
| # reviewer invitation) is DRAFTED for the curation team. Never sent by code; | |
| # never repeated; skipped when the author excluded newsletters, held, withdrew, | |
| # or is already on the website registries. See intake/post_publication.py. | |
| FOLLOWUP_DAYS = int(os.environ.get("ICSAC_FOLLOWUP_DAYS", "7")) | |
| SITE_BASE_URL = "https://icsacinstitute.org" | |
| # Suffix pattern; fields: {year} {seq} {sub_id}. seq is per-year, persisted in | |
| # CROSSREF_SEQ_FILE. Drafts can be re-staged under a new pattern for free; | |
| # a registered DOI cannot be changed. | |
| # The suffix is the Institute's, not the journal's: the journal lives in the | |
| # deposit metadata (journal_metadata + the volume block below), so the DOI | |
| # string never changes if the serial is ever renamed or split. | |
| CROSSREF_DOI_SUFFIX = os.environ.get("CROSSREF_DOI_SUFFIX", "icsac.{year}.{seq:03d}") | |
| CROSSREF_SEQ_FILE = os.environ.get("CROSSREF_SEQ_FILE", | |
| os.path.expanduser("~/icsac-submissions/.doi-seq")) | |
| # "journal-article" (DEFAULT since 2026-09-28): Persistence is the journal of | |
| # record, published online at icsacinstitute.org. Every accepted paper is an | |
| # article of the open volume from the day its DOI registers; the paperback and | |
| # ebook are that volume's annual edition (print inclusion is not guaranteed; | |
| # policy at /journal#how-it-works). The print date and pages are added later by | |
| # redepositing the same DOI. OpenAlex reads "article" once an ISSN is attached. | |
| # "report-paper": a paper the Institute publishes outside the journal | |
| # (explicit <publisher> = ICSAC; OpenAlex type "report"). | |
| # "posted_content": Crossref's preprint class (OpenAlex labels it "preprint"). | |
| CROSSREF_CONTENT_TYPE = os.environ.get("CROSSREF_CONTENT_TYPE", "journal-article") | |
| CROSSREF_JOURNAL_TITLE = "Persistence" | |
| CROSSREF_JOURNAL_ABBREV = "" # none registered yet | |
| CROSSREF_JOURNAL_ISSN = os.environ.get("CROSSREF_JOURNAL_ISSN", "") # eISSN pending (LoC refile >= 2026-11-12) | |
| # The open volume: everything accepted until a volume's print cutoff is deposited | |
| # as an article of it. Bump BOTH by hand when the next volume opens (Volume 1 | |
| # closes with the May 2027 edition). The year is the volume's online year: the | |
| # schema requires a date on the volume block; the print date comes at redeposit. | |
| # An empty CROSSREF_JOURNAL_VOLUME omits the volume block entirely. | |
| CROSSREF_JOURNAL_VOLUME = os.environ.get("CROSSREF_JOURNAL_VOLUME", "1") | |
| CROSSREF_JOURNAL_VOLUME_YEAR = os.environ.get("CROSSREF_JOURNAL_VOLUME_YEAR", "2026") | |
| CROSSREF_POSTED_TYPE = "other" # only used for posted_content | |
| CROSSREF_DEPOSITOR_NAME = "ICSAC" | |
| CROSSREF_DEPOSITOR_EMAIL = "help@icsacinstitute.org" | |
| CROSSREF_REGISTRANT = "Institute for Complexity Science and Advanced Computing" | |
| CROSSREF_PUBLISHER_NAME = CROSSREF_REGISTRANT | |
| CROSSREF_PUBLISHER_PLACE = "Fort Wayne, Indiana, USA" | |
| # Same-origin PDF the deposit points Similarity Check / text-mining at. | |
| # register --live copies paper.pdf to <website>/public/papers/<sub_id>.pdf. | |
| CROSSREF_PDF_URL = "https://icsacinstitute.org/papers/{sub_id}.pdf" | |
| CROSSREF_SCHEMA_DIR = os.environ.get("CROSSREF_SCHEMA_DIR", | |
| os.path.expanduser("~/Desktop/icsac/crossref-schemas")) | |
| # Deposit credentials: the technical contact's personal Crossref login + role. | |
| # Live in ~/.config/crossref.env (loaded by intake/register-doi.sh), never here. | |
| CROSSREF_LOGIN_EMAIL = os.environ.get("CROSSREF_LOGIN_EMAIL", "") | |
| CROSSREF_ROLE = os.environ.get("CROSSREF_ROLE", "") | |
| CROSSREF_PASSWORD = os.environ.get("CROSSREF_PASSWORD", "") | |
| # ββ CiteStamp citation-graph check (Phase 3 of citation verification) ββββββββ | |
| # Public MCP endpoint, stateless JSON-RPC, no token needed; a token raises the | |
| # anonymous rate ceiling if one is ever issued to the pipeline (env only). | |
| CITESTAMP_MCP_URL = os.environ.get("CITESTAMP_MCP_URL", "https://mcp.citestamp.com/mcp") | |
| CITESTAMP_MCP_TOKEN = os.environ.get("CITESTAMP_MCP_TOKEN", "") | |
| # Self-heal thresholds (claude + 4 OR slots = 5 total panelists per pass). | |
| # MIN_REVIEWERS=4 tolerates 1 slot failure per pass after self-heal retry. | |
| # Combined with REVIEW_PASSES below, a paper yields 8-10 valid reviews in | |
| # the aggregate. Tightened from MIN_REVIEWERS=3 + 3 passes 2026-04-26 | |
| # after observing pass-to-pass stdev was uniformly tiny (β€0.41 on the | |
| # noisiest dim, β€0.09 on most) β 3rd pass added marginal stderr at 33% | |
| # more compute. Two passes captures essentially the same signal; pairing | |
| # with MIN_REVIEWERS=4 keeps each pass closer to full panel. | |
| MIN_REVIEWERS = 4 | |
| MAX_SLOT_RETRIES = 1 # per failed slot, after the initial attempt | |
| RETRY_COOLDOWN_SEC = 30 # wait between initial pass and retry pass | |
| # Multi-pass aggregation: run the full panel N times, aggregate mean+stdev | |
| # across passes. 2 passes balances stability against compute. Set to 1 | |
| # to disable multi-pass; 3+ for noise-reduction at compute cost. | |
| REVIEW_PASSES = 2 | |
| SMTP_HOST = os.environ.get("SMTP_HOST", "smtp.gmail.com") | |
| SMTP_PORT = int(os.environ.get("SMTP_PORT", "465")) | |
| SMTP_USER = os.environ.get("SMTP_USER", "") | |
| SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD", "") | |
| FROM_EMAIL = os.environ.get("FROM_EMAIL", "info@icsacinstitute.org") | |
| REPLY_TO_EMAIL = os.environ.get("REPLY_TO_EMAIL", "info@icsacinstitute.org") | |
| NTFY_PAIN_URL = os.environ.get("NTFY_PAIN_URL", "") | |
| NTFY_BACKUPS_URL = os.environ.get("NTFY_BACKUPS_URL", "") | |
| BRAIN_URL = os.environ.get("BRAIN_URL", "") | |
| KUMA_PUSH_URL = os.environ.get("KUMA_PUSH_URL", "") | |
| COMMUNITY_ID = "icsac" | |
| GOOGLE_FORM_URL = os.environ.get("GOOGLE_FORM_URL", "https://example.com/your-community-signup-form") | |
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| # Runtime output directory β created on first run, gitignored. | |
| REVIEWS_DIR = os.path.join(BASE_DIR, "reviews") | |
| # Runtime output directory β created on first run, gitignored. | |
| DOWNLOADS_DIR = os.path.join(BASE_DIR, "downloads") | |
| RUBRICS_DIR = os.path.join(BASE_DIR, "rubrics") | |
| TEMPLATES_DIR = os.path.join(BASE_DIR, "templates") | |
| # Site base URL used to build share-target landing pages (icsacinstitute.org/accepted/<id>) | |
| SITE_BASE_URL = "https://icsacinstitute.org" | |
| CLAUDE_CMD = "claude" | |
| # DEPRECATED (2026-05-22): unused. The gemini-cli free tier sunsets | |
| # 2026-06-18. Blind-review compaction now uses CLAUDE_CMD, and the panel's | |
| # Gemini-family voice is served via OpenRouter google/gemma :free models. | |
| # No remaining code path invokes this binary. Kept only to avoid an | |
| # AttributeError in any external fork that still references it. | |
| GEMINI_CMD = "gemini" | |
| RUBRIC_DIMENSIONS = [ | |
| "domain_fit", | |
| "methodological_transparency", | |
| "internal_consistency", | |
| "citation_integrity", | |
| "novelty_signal", | |
| "ai_provenance_signal", | |
| ] | |
| # Code package digest for the panel (2026-09-30, code_digest.py): the author's | |
| # declared Zenodo/GitHub archive is fetched READ-ONLY into memory, never | |
| # executed, capped, and its file list + definitions + the lines defining the | |
| # paper's named constructs go into the prompt. ICSAC_CODE_DIGEST=0 turns the | |
| # fetch off (the prompt then says "(none)" and the panel treats code claims as | |
| # unverified). | |
| CODE_DIGEST_ENABLED = os.environ.get("ICSAC_CODE_DIGEST", "1") != "0" | |
| CODE_DIGEST_MAX_BYTES = int(os.environ.get("ICSAC_CODE_DIGEST_MAX_BYTES", str(25 * 1024 * 1024))) | |
| CODE_DIGEST_MAX_CHARS = int(os.environ.get("ICSAC_CODE_DIGEST_MAX_CHARS", "14000")) | |
| # Path to the institute's website repo (used by publications.py to commit | |
| # accepted-paper landing pages + redacted reviews). Empty disables the | |
| # website-registry push; the Zenodo accept itself still proceeds. | |
| ICSAC_WEBSITE_REPO = os.environ.get("ICSAC_WEBSITE_REPO", "") | |