references/materials-workflow.md
# Software copyright materials workflow
Use this reference for China software copyright material preparation. It is not legal advice and does not submit applications.
## 1. Official-rule posture
Default jurisdiction is `CN` only when the user asks for China software copyright. Do not reuse CN rules for US/EU work. Recheck official pages on the day of real filing. Preserve `UNKNOWN` or `PLANNED` when form, page, fee, signature or portal facts are not verified.
## 2. Business understanding before forms
Before writing application fields or a manual, read real project evidence:
- README, PRD/BRD/design docs, route/page/component names, API docs, CLI help, screenshots, product text and user-provided descriptions.
- Identify domain, target users, main functions, technical characteristics, operation flow and system requirements.
- Distinguish project evidence from external industry references.
The manual should read like a real user/operation manual: where the user enters, what they see, what they click/type, what validation/error appears and what result is produced. Avoid code implementation, framework internals, marketing slogans and uniform AI-style section templates.
## 3. Source deposit plan
Run the source planner before code extraction:
```bash
python scripts/source_deposit_plan.py --root <project-root> --out 软件著作权申请资料/草稿/source-deposit-plan.json --software-name "<软件全称>" --version "<版本号>"
python scripts/source_deposit_plan.py --selftest
```
Use the plan to inspect:
- candidate source files, relative paths and SHA-256;
- source/material line counts and estimated pages;
- recommended files and model reasons;
- `front_back_30` vs `all_source` mode;
- secret findings.
If `secret_scan.status` is `FOUND`, stop for redaction. Do not export code material until the scan is `PASS` or `REDACTED_AND_VERIFIED`.
## 4. Code selection rules
Select real source that best represents the registered software expression:
- Prefer entry, routes/pages, components, API/services, state/data handling, core backend/business logic and user-visible workflows.
- Exclude dependencies, build output, minified bundles, generated files, lock files, vendored/unrelated third-party code and private data.
- If selected source is fewer than 60 pages and all relevant source is fewer than 60 pages, use all real source; never pad or AI-generate code.
- If selected source is fewer than 60 pages but relevant candidate source can reach 60 pages, ask the user/model to supplement selection before extraction.
- Keep the plan hash and confirmation basis in the materials packet.
## 5. Rights and third-party review
Before formal export, record:
- `rights_basis`: independent/cooperative/commissioned/assigned/inherited/derivative basis and supporting locator if available.
- `third_party_code_review`: whether third-party, open-source, generated, template, dataset, model-weight or unrelated code is included or excluded.
If rights are not verified, do not mark the packet PASS. If third-party code needs exclusion, list excluded relative paths.
## 6. Application worksheet
Keep these fields consistent across all materials:
- software full name, optional abbreviation, version;
- copyright owner/applicant, developer and development mode;
- completion/publication dates;
- development/operation hardware and software environment;
- programming languages, source program quantity, document quantity;
- development purpose, domain, main functions and technical characteristics.
Names and versions in worksheet, manual title/header, code material header and formal filenames must match.
## 7. Documentation material and screenshots
The documentation material can be an operation/user manual, design document or other official-accepted document depending on current rules. For normal material logic:
- document pages at least 60: front/back 30 pages;
- fewer than 60: include all;
- full document pages normally use at least 30 lines unless an official exception is documented.
Screenshots are optional depending on workflow and local practice. If used, bind each screenshot to real running UI or user-provided image with SHA-256. If skipped, record why and keep visible screenshot placeholders when the manual expects them.
## 8. Gate before review
Create `software-copyright-materials.json`, then run:
```bash
python scripts/materials_gate.py --packet software-copyright-materials.json --base <project-root> --as-of 2026-07-05
python scripts/materials_gate.py --selftest
```
The gate checks jurisdiction, legal boundary, application fields, source plan, source files, confirmations, rights basis, third-party review, code/page rules, document/page rules, version consistency, secret scan, screenshots and formal-output hashes.
references/software-copyright-resource-map.md
# Software copyright resource map
Last checked: 2026-07-05. Software copyright forms, portals and acceptance
requirements can change; verify official pages again before real submission.
## Peer skills and what Light borrowed
| Source | Observed signal | Useful mechanism | Light boundary |
|---|---:|---|---|
| [`Fokkyp/SoftwareCopyright-Skill`](https://github.com/Fokkyp/SoftwareCopyright-Skill) | ~4.2k stars, MIT, HEAD `0a27d1143eca253b7b7e953794a24ef6e5488b0d` | Real-project analysis, application worksheet, business-context/manual drafting, front/back 30 source extraction, staged confirmations, formal folder | Light adds a machine evidence gate, stricter secret/version/hash checks and avoids “可提交” overclaim |
| [`na57/chinese-copyright-application-skill`](https://github.com/na57/chinese-copyright-application-skill) | ~151 stars, HEAD `04081d0` | China-oriented application fields, manual/source-material templates | Lower process fidelity; use as checklist inspiration only |
| [`jaccen/AI-Copyright-Skill`](https://github.com/jaccen/AI-Copyright-Skill) | Adjacent patent + soft-copyright skill, HEAD `20908b7` | IP material framing and combined documentation prompts | Do not import AI-specific assumptions or generated-code shortcuts |
Deep read record: [`../../../docs/learning/ip/software-copyright-deep-read-report.md`](../../../docs/learning/ip/software-copyright-deep-read-report.md).
## Official and high-trust anchors
- National Copyright Administration PDF, `计算机软件著作权登记办法`:
<https://www.ncac.gov.cn/xxfb/flfg/bmgz/202410/P020241015604759788122.pdf>.
It anchors the application materials, China Copyright Protection Center role,
software identification materials, first/last 30-page logic, all-material
logic under 60 pages, line-count norms and unified Chinese/A4 form language.
- Beijing government service guide, `计算机软件著作权登记初审`:
<https://banshi.beijing.gov.cn/pubtask/task/1/110000000000/3e283672-76be-4c8c-98e8-0bebe9bd06bf.html?locationCode=110000000000>.
Useful as a service-guide consolidation of current material requirements,
signature/seal notes and version consistency reminders.
- U.S. Copyright Office Circular 61:
<https://www.copyright.gov/circs/circ61.pdf>.
Use only when the user explicitly asks for US computer-program registration;
US deposit rules differ from China and must not be mixed.
- `计算机软件保护条例`:
<https://xzfg.moj.gov.cn/front/law/detail?LawID=914>.
Use for the boundary that software copyright protects expression, not ideas,
processing methods, operation methods or mathematical concepts.
## Light material packet contract
A credible China software copyright packet has:
1. `jurisdiction: CN` and explicit non-legal-advice acknowledgement;
2. application fields with user-confirmed software name and version;
3. a real source-file manifest with relative paths and SHA-256;
4. code material from real source, with page mode and line-count rule;
5. documentation/manual material with page mode and line-count rule;
6. confirmation gates for environment, application fields, business context,
code selection, markdown draft and final export;
7. version/name consistency evidence across worksheet, manual, code headers and
formal output filenames;
8. secret/private-data scan result;
9. formal output file paths and SHA-256.
10. source-deposit plan SHA, verified rights basis and third-party/open-source
code review.
The machine gate is [`../scripts/materials_gate.py`](../scripts/materials_gate.py).
It intentionally blocks:
- AI-generated or padded source code;
- missing user confirmations;
- front/back page-rule mismatch;
- software version mismatch;
- missing formal output hashes;
- secret-scan failures;
- submission or approval guarantees.
The lightweight source planner is
[`../scripts/source_deposit_plan.py`](../scripts/source_deposit_plan.py). It
creates `light.software_copyright.source_deposit_plan.v1` records with
candidate files, SHA-256, line/page estimates, recommended selection and a
secret scan. The model/user must still confirm the final selection.
## Prompting stance
The useful expert posture is:
> “I will package what your real software already contains, keep every field and
> version consistent, show exactly which source files became code material, and
> stop where ownership, publication, third-party code, secrets or official-rule
> changes need human/professional confirmation.”
If there is no real project source, produce a readiness checklist instead of
creating fake registration material.
scripts/materials_gate.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Validate Chinese software copyright registration material evidence.
This gate checks provenance, page/line rules, version-name consistency and
confirmation stops. It does not submit materials and does not provide legal
advice.
"""
from __future__ import annotations
import argparse
import copy
import datetime as dt
import hashlib
import json
import pathlib
import re
import shutil
import sys
import tempfile
from typing import Any
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
SCHEMA_ID = "light.software_copyright.materials.v1"
REPORT_SCHEMA = "light.software_copyright.gate_report.v1"
SHA_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
PLACEHOLDER_RE = re.compile(r"(<[^>]+>|\bTBD\b|\bTODO\b|待填|待补|示例|example)", re.I)
OVERCLAIM_RE = re.compile(
r"(包过|保过|包下证|保证登记|可直接提交|自动提交|无需审查|ready\s*to\s*submit)",
re.I,
)
SECRET_STATUS_ALLOWED = {"PASS", "REDACTED_AND_VERIFIED"}
SOURCE_PLAN_SCHEMA = "light.software_copyright.source_deposit_plan.v1"
RIGHTS_STATUS_ALLOWED = {"VERIFIED", "PLANNED", "UNKNOWN", "UNAVAILABLE"}
THIRD_PARTY_STATUS_ALLOWED = {"PASS", "REVIEWED_WITH_EXCLUSIONS"}
CONFIRMATION_KEYS = (
"environment",
"source_plan",
"application_fields",
"business_context",
"code_selection",
"markdown_draft",
"final_export",
)
APPLICATION_FIELDS = (
"software_full_name",
"version",
"copyright_owner",
"development_completion_date",
"first_publication_status",
"development_mode",
)
def repo_root(start: str | pathlib.Path | None = None) -> pathlib.Path:
cur = pathlib.Path(start or __file__).resolve()
if cur.is_file():
cur = cur.parent
while cur != cur.parent and not (cur / "skills").is_dir():
cur = cur.parent
if not (cur / "skills").is_dir():
raise RuntimeError("Light-Skills repository root not found")
return cur
def issue(code: str, path: str, message: str, severity: str = "ERROR") -> dict[str, str]:
return {"severity": severity, "code": code, "path": path, "message": message}
def file_sha256(path: pathlib.Path) -> str:
h = hashlib.sha256()
with path.open("rb") as fh:
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
h.update(chunk)
return "sha256:" + h.hexdigest()
def load_json(path: pathlib.Path) -> Any:
return json.loads(path.read_text(encoding="utf-8-sig"))
def strings(value: Any) -> list[str]:
if isinstance(value, str):
return [value]
if isinstance(value, list):
out: list[str] = []
for item in value:
out.extend(strings(item))
return out
if isinstance(value, dict):
out = []
for item in value.values():
out.extend(strings(item))
return out
return []
def scalar_ok(value: Any) -> bool:
return isinstance(value, str) and bool(value.strip()) and not PLACEHOLDER_RE.search(value)
def path_problem(raw: Any) -> str | None:
if not isinstance(raw, str) or not raw.strip():
return "path must be a non-empty string"
value = raw.strip()
win = pathlib.PureWindowsPath(value)
posix = pathlib.PurePosixPath(value)
if win.is_absolute() or posix.is_absolute() or win.drive:
return "path must be project-relative, not absolute"
if value.startswith("~") or "\x00" in value:
return "path contains unsafe prefix or null byte"
if ".." in win.parts or ".." in posix.parts:
return "path must not escape with '..'"
if PLACEHOLDER_RE.search(value):
return "path contains placeholder text"
return None
def parse_date(value: Any, path: str, as_of: dt.date, problems: list[dict[str, str]]) -> dt.date | None:
if not isinstance(value, str) or not value.strip():
problems.append(issue("DATE_MISSING", path, "date must be YYYY-MM-DD"))
return None
try:
parsed = dt.date.fromisoformat(value[:10])
except ValueError:
problems.append(issue("DATE_INVALID", path, "date must be YYYY-MM-DD"))
return None
if parsed > as_of:
problems.append(issue("DATE_IN_FUTURE", path, f"date {parsed.isoformat()} is after as_of"))
return parsed
def validate_file_list(items: Any, base: pathlib.Path | None, path: str) -> list[dict[str, str]]:
problems: list[dict[str, str]] = []
if not isinstance(items, list) or not items:
return [issue("FILE_LIST_MISSING", path, "at least one file entry is required")]
seen_ids: set[str] = set()
for index, item in enumerate(items):
item_path = f"{path}[{index}]"
if not isinstance(item, dict):
problems.append(issue("FILE_ENTRY_INVALID", item_path, "file entry must be an object"))
continue
file_id = item.get("id")
if scalar_ok(file_id):
if str(file_id) in seen_ids:
problems.append(issue("FILE_ID_DUPLICATE", f"{item_path}.id", "file ids must be unique"))
seen_ids.add(str(file_id))
else:
problems.append(issue("FILE_ID_MISSING", f"{item_path}.id", "file id required"))
locator = item.get("path")
if problem := path_problem(locator):
problems.append(issue("FILE_PATH_INVALID", f"{item_path}.path", problem))
continue
sha = item.get("sha256")
if not isinstance(sha, str) or not SHA_RE.match(sha):
problems.append(issue("FILE_HASH_INVALID", f"{item_path}.sha256", "sha256:<64 lowercase hex> required"))
if base is not None and isinstance(locator, str):
resolved = (base / locator).resolve()
try:
resolved.relative_to(base.resolve())
except ValueError:
problems.append(issue("FILE_PATH_ESCAPE", f"{item_path}.path", "resolved path escapes base"))
continue
if not resolved.is_file():
problems.append(issue("FILE_MISSING", f"{item_path}.path", "referenced file does not exist"))
elif isinstance(sha, str) and SHA_RE.match(sha) and file_sha256(resolved) != sha:
problems.append(issue("FILE_HASH_MISMATCH", f"{item_path}.sha256", "sha256 does not match file"))
return problems
def validate_confirmations(packet: dict[str, Any], as_of: dt.date) -> list[dict[str, str]]:
problems: list[dict[str, str]] = []
confirmations = packet.get("stage_confirmations")
if not isinstance(confirmations, dict):
return [issue("CONFIRMATIONS_MISSING", "stage_confirmations", "confirmation gates are required")]
for key in CONFIRMATION_KEYS:
path = f"stage_confirmations.{key}"
item = confirmations.get(key)
if not isinstance(item, dict):
problems.append(issue("CONFIRMATION_MISSING", path, "confirmation object required"))
continue
if item.get("confirmed") is not True:
problems.append(issue("CONFIRMATION_NOT_TRUE", f"{path}.confirmed", "must be true before formal export"))
parse_date(item.get("confirmed_at"), f"{path}.confirmed_at", as_of, problems)
if not scalar_ok(item.get("basis")):
problems.append(issue("CONFIRMATION_BASIS_MISSING", f"{path}.basis", "basis/locator for user or maintainer confirmation required"))
return problems
def validate_source_plan(packet: dict[str, Any], base: pathlib.Path | None) -> list[dict[str, str]]:
problems: list[dict[str, str]] = []
plan = packet.get("source_plan")
if not isinstance(plan, dict):
return [issue("SOURCE_PLAN_MISSING", "source_plan", "source deposit plan generated from real project source is required")]
if plan.get("schema") != SOURCE_PLAN_SCHEMA:
problems.append(issue("SOURCE_PLAN_SCHEMA_INVALID", "source_plan.schema", f"expected {SOURCE_PLAN_SCHEMA}"))
plan_sha = plan.get("sha256")
if not isinstance(plan_sha, str) or not SHA_RE.match(plan_sha):
problems.append(issue("SOURCE_PLAN_HASH_MISSING", "source_plan.sha256", "source plan sha256 required"))
if plan.get("confirmed") is not True:
problems.append(issue("SOURCE_PLAN_UNCONFIRMED", "source_plan.confirmed", "source plan must be confirmed before formal export"))
if plan.get("selected_file_count") is not None and (not isinstance(plan.get("selected_file_count"), int) or plan.get("selected_file_count") <= 0):
problems.append(issue("SOURCE_PLAN_SELECTED_COUNT_INVALID", "source_plan.selected_file_count", "selected_file_count must be positive when present"))
if plan.get("deposit_mode") not in {"front_back_30", "all_source"}:
problems.append(issue("SOURCE_PLAN_DEPOSIT_MODE_INVALID", "source_plan.deposit_mode", "use front_back_30 or all_source"))
if plan.get("secret_scan_status") not in SECRET_STATUS_ALLOWED:
problems.append(issue("SOURCE_PLAN_SECRET_STATUS_INVALID", "source_plan.secret_scan_status", "source plan secret scan must be PASS or REDACTED_AND_VERIFIED"))
locator = plan.get("path")
if locator is not None:
if problem := path_problem(locator):
problems.append(issue("SOURCE_PLAN_PATH_INVALID", "source_plan.path", problem))
elif base is not None:
resolved = (base / str(locator)).resolve()
try:
resolved.relative_to(base.resolve())
except ValueError:
problems.append(issue("SOURCE_PLAN_PATH_ESCAPE", "source_plan.path", "resolved path escapes base"))
else:
if not resolved.is_file():
problems.append(issue("SOURCE_PLAN_FILE_MISSING", "source_plan.path", "source plan file does not exist"))
elif isinstance(plan_sha, str) and SHA_RE.match(plan_sha) and file_sha256(resolved) != plan_sha:
problems.append(issue("SOURCE_PLAN_HASH_MISMATCH", "source_plan.sha256", "sha256 does not match source plan file"))
return problems
def validate_rights_basis(packet: dict[str, Any]) -> list[dict[str, str]]:
problems: list[dict[str, str]] = []
rights = packet.get("rights_basis")
if not isinstance(rights, dict):
return [issue("RIGHTS_BASIS_MISSING", "rights_basis", "development mode and rights-acquisition basis must be recorded")]
status = str(rights.get("status", "")).upper()
if status not in RIGHTS_STATUS_ALLOWED:
problems.append(issue("RIGHTS_STATUS_INVALID", "rights_basis.status", "use VERIFIED, PLANNED, UNKNOWN or UNAVAILABLE"))
if status != "VERIFIED":
problems.append(issue("RIGHTS_NOT_VERIFIED", "rights_basis.status", "rights basis must be VERIFIED before formal export PASS"))
for field in ("development_mode", "basis"):
if not scalar_ok(rights.get(field)):
problems.append(issue("RIGHTS_FIELD_MISSING", f"rights_basis.{field}", "non-placeholder value required"))
return problems
def validate_third_party_code_review(packet: dict[str, Any]) -> list[dict[str, str]]:
problems: list[dict[str, str]] = []
review = packet.get("third_party_code_review")
if not isinstance(review, dict):
return [issue("THIRD_PARTY_REVIEW_MISSING", "third_party_code_review", "third-party/open-source/unrelated code review is required")]
status = review.get("status")
if status not in THIRD_PARTY_STATUS_ALLOWED:
problems.append(issue("THIRD_PARTY_REVIEW_STATUS_INVALID", "third_party_code_review.status", "use PASS or REVIEWED_WITH_EXCLUSIONS before export"))
if not scalar_ok(review.get("summary")):
problems.append(issue("THIRD_PARTY_REVIEW_SUMMARY_MISSING", "third_party_code_review.summary", "review summary required"))
if status == "REVIEWED_WITH_EXCLUSIONS":
excluded = review.get("excluded_paths")
if not isinstance(excluded, list) or not excluded:
problems.append(issue("THIRD_PARTY_EXCLUSIONS_MISSING", "third_party_code_review.excluded_paths", "excluded paths required when exclusions were used"))
else:
for index, path in enumerate(excluded):
if problem := path_problem(path):
problems.append(issue("THIRD_PARTY_EXCLUSION_PATH_INVALID", f"third_party_code_review.excluded_paths[{index}]", problem))
return problems
def validate_code_material(code: Any) -> list[dict[str, str]]:
problems: list[dict[str, str]] = []
if not isinstance(code, dict):
return [issue("CODE_MATERIAL_MISSING", "code_material", "code material object required")]
if code.get("source_type") != "REAL_PROJECT":
problems.append(issue("CODE_NOT_REAL_PROJECT", "code_material.source_type", "code material must come from real project source"))
if code.get("selection_confirmed") is not True:
problems.append(issue("CODE_SELECTION_UNCONFIRMED", "code_material.selection_confirmed", "human/model selection must be confirmed before extraction"))
if not isinstance(code.get("extraction_manifest_sha256"), str) or not SHA_RE.match(code["extraction_manifest_sha256"]):
problems.append(issue("CODE_MANIFEST_HASH_MISSING", "code_material.extraction_manifest_sha256", "extraction manifest sha256 required"))
if not isinstance(code.get("source_plan_sha256"), str) or not SHA_RE.match(code["source_plan_sha256"]):
problems.append(issue("CODE_SOURCE_PLAN_HASH_MISSING", "code_material.source_plan_sha256", "source deposit plan sha256 required"))
total_pages = code.get("total_source_pages")
mode = code.get("deposit_mode")
if not isinstance(total_pages, int) or total_pages <= 0:
problems.append(issue("CODE_TOTAL_PAGES_INVALID", "code_material.total_source_pages", "positive integer required"))
total_pages = 0
exceptional = code.get("exceptional_deposit") is True
if total_pages >= 60 and not exceptional:
if mode != "front_back_30":
problems.append(issue("CODE_DEPOSIT_MODE_INVALID", "code_material.deposit_mode", ">=60 source pages require front_back_30 unless exceptional_deposit is true"))
if code.get("front_pages") != 30 or code.get("back_pages") != 30:
problems.append(issue("CODE_FRONT_BACK_INVALID", "code_material", "front_pages and back_pages must both be 30"))
if 0 < total_pages < 60:
if mode != "all_source" or code.get("all_source_included") is not True:
problems.append(issue("CODE_ALL_SOURCE_REQUIRED", "code_material", "<60 source pages require all source pages"))
if code.get("full_code_pages_have_min_50_lines") is not True:
problems.append(issue("CODE_LINE_RULE_MISSING", "code_material.full_code_pages_have_min_50_lines", "full source pages must meet the 50-line rule or document an official exception"))
if code.get("mid_file_snippet_used") is True and not exceptional:
problems.append(issue("CODE_MID_SNIPPET_USED", "code_material.mid_file_snippet_used", "do not fabricate arbitrary middle snippets for normal deposit"))
return problems
def validate_document_material(document: Any) -> list[dict[str, str]]:
problems: list[dict[str, str]] = []
if not isinstance(document, dict):
return [issue("DOCUMENT_MATERIAL_MISSING", "document_material", "manual/document material object required")]
total_pages = document.get("total_document_pages")
mode = document.get("deposit_mode")
if not isinstance(total_pages, int) or total_pages <= 0:
problems.append(issue("DOCUMENT_TOTAL_PAGES_INVALID", "document_material.total_document_pages", "positive integer required"))
total_pages = 0
if total_pages >= 60:
if mode != "front_back_30":
problems.append(issue("DOCUMENT_DEPOSIT_MODE_INVALID", "document_material.deposit_mode", ">=60 document pages require front_back_30"))
if document.get("front_pages") != 30 or document.get("back_pages") != 30:
problems.append(issue("DOCUMENT_FRONT_BACK_INVALID", "document_material", "front_pages and back_pages must both be 30"))
if 0 < total_pages < 60 and (mode != "all_document" or document.get("all_document_included") is not True):
problems.append(issue("DOCUMENT_ALL_REQUIRED", "document_material", "<60 document pages require all pages"))
if document.get("full_document_pages_have_min_30_lines") is not True:
problems.append(issue("DOCUMENT_LINE_RULE_MISSING", "document_material.full_document_pages_have_min_30_lines", "full document pages must meet the 30-line rule or document an official exception"))
if document.get("business_context_confirmed") is not True:
problems.append(issue("BUSINESS_CONTEXT_UNCONFIRMED", "document_material.business_context_confirmed", "manual must be grounded in confirmed business/product context"))
return problems
def validate(packet: dict[str, Any], base: pathlib.Path | None, as_of: dt.date) -> dict[str, Any]:
problems: list[dict[str, str]] = []
if packet.get("schema") != SCHEMA_ID:
problems.append(issue("SCHEMA_MISMATCH", "schema", f"expected {SCHEMA_ID}"))
if packet.get("jurisdiction") != "CN":
problems.append(issue("JURISDICTION_UNSUPPORTED", "jurisdiction", "this gate is for China software copyright materials; use CN"))
if packet.get("not_legal_advice_ack") is not True:
problems.append(issue("LEGAL_ACK_MISSING", "not_legal_advice_ack", "must be true; this skill is not legal advice"))
if OVERCLAIM_RE.search("\n".join(strings(packet))):
problems.append(issue("OVERCLAIM", "$", "remove guarantee/auto-submit/ready-to-submit language"))
if packet.get("submission", {}).get("submit_to_office") is True:
problems.append(issue("SUBMISSION_OVERSTEP", "submission.submit_to_office", "Light must not submit registration materials"))
application = packet.get("application")
if not isinstance(application, dict):
problems.append(issue("APPLICATION_MISSING", "application", "application fields required"))
application = {}
for field in APPLICATION_FIELDS:
if not scalar_ok(application.get(field)):
problems.append(issue("APPLICATION_FIELD_MISSING", f"application.{field}", "non-placeholder value required"))
parse_date(application.get("development_completion_date"), "application.development_completion_date", as_of, problems)
binding = packet.get("project_binding")
if not isinstance(binding, dict):
problems.append(issue("PROJECT_BINDING_MISSING", "project_binding", "project binding and source file manifest required"))
else:
if not scalar_ok(binding.get("source_root_name")):
problems.append(issue("SOURCE_ROOT_NAME_MISSING", "project_binding.source_root_name", "source root name required"))
if not isinstance(binding.get("manifest_sha256"), str) or not SHA_RE.match(binding["manifest_sha256"]):
problems.append(issue("MANIFEST_HASH_MISSING", "project_binding.manifest_sha256", "source manifest sha256 required"))
problems.extend(validate_file_list(binding.get("source_files"), base, "project_binding.source_files"))
problems.extend(validate_confirmations(packet, as_of))
problems.extend(validate_source_plan(packet, base))
problems.extend(validate_rights_basis(packet))
problems.extend(validate_third_party_code_review(packet))
problems.extend(validate_code_material(packet.get("code_material")))
problems.extend(validate_document_material(packet.get("document_material")))
version = application.get("version")
consistency = packet.get("version_consistency")
if not isinstance(consistency, dict):
problems.append(issue("VERSION_CONSISTENCY_MISSING", "version_consistency", "version consistency evidence required"))
else:
if consistency.get("all_materials_match_application") is not True:
problems.append(issue("VERSION_MISMATCH_DECLARED", "version_consistency.all_materials_match_application", "all material versions must match application version"))
materials = consistency.get("materials")
if not isinstance(materials, list) or not materials:
problems.append(issue("VERSION_MATERIALS_MISSING", "version_consistency.materials", "material version list required"))
else:
for index, item in enumerate(materials):
if not isinstance(item, dict):
problems.append(issue("VERSION_MATERIAL_INVALID", f"version_consistency.materials[{index}]", "material must be object"))
continue
if item.get("version") != version:
problems.append(issue("VERSION_MISMATCH", f"version_consistency.materials[{index}].version", "material version must match application.version"))
if not scalar_ok(item.get("locator")):
problems.append(issue("VERSION_LOCATOR_MISSING", f"version_consistency.materials[{index}].locator", "version locator required"))
secret = packet.get("secret_scan")
if not isinstance(secret, dict) or secret.get("status") not in SECRET_STATUS_ALLOWED:
problems.append(issue("SECRET_SCAN_NOT_CLEAN", "secret_scan.status", "status must be PASS or REDACTED_AND_VERIFIED before exporting source code"))
if isinstance(packet.get("source_plan"), dict) and isinstance(packet.get("code_material"), dict):
if packet["source_plan"].get("sha256") != packet["code_material"].get("source_plan_sha256"):
problems.append(issue("SOURCE_PLAN_HASH_INCONSISTENT", "code_material.source_plan_sha256", "must match source_plan.sha256"))
screenshot = packet.get("screenshots")
if isinstance(screenshot, dict) and screenshot.get("required") is True:
problems.extend(validate_file_list(screenshot.get("files"), base, "screenshots.files"))
elif isinstance(screenshot, dict) and screenshot.get("required") is False:
if not scalar_ok(screenshot.get("skip_reason")):
problems.append(issue("SCREENSHOT_SKIP_REASON_MISSING", "screenshots.skip_reason", "record why screenshots are not included"))
formal = packet.get("formal_output")
if not isinstance(formal, dict):
problems.append(issue("FORMAL_OUTPUT_MISSING", "formal_output", "formal output list required"))
else:
if formal.get("generated") is not True:
problems.append(issue("FORMAL_OUTPUT_NOT_GENERATED", "formal_output.generated", "formal files must be generated before PASS"))
problems.extend(validate_file_list(formal.get("files"), base, "formal_output.files"))
return {
"schema": REPORT_SCHEMA,
"verdict": "FAIL" if any(row["severity"] == "ERROR" for row in problems) else "PASS",
"checked_at": dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat(),
"as_of": as_of.isoformat(),
"packet_schema": packet.get("schema"),
"issues": problems,
}
def run_selftest() -> int:
root = repo_root()
e2e_root = root / ".upgrade" / "_e2e" / "software-copyright-gate"
e2e_root.mkdir(parents=True, exist_ok=True)
tmp = pathlib.Path(tempfile.mkdtemp(prefix="selftest-", dir=e2e_root))
try:
src = tmp / "src"
src.mkdir()
app_py = src / "app.py"
app_py.write_text("print('real project source')\n", encoding="utf-8")
plan_file = tmp / "source-deposit-plan.json"
plan_file.write_text('{"schema":"light.software_copyright.source_deposit_plan.v1","selected_file_count":1}\n', encoding="utf-8")
out = tmp / "软件著作权申请资料" / "正式资料"
out.mkdir(parents=True)
manual = out / "LightDemo_操作手册.docx"
code = out / "LightDemo-代码材料.docx"
form = out / "申请表信息.txt"
for path in (manual, code, form):
path.write_text("placeholder binary surrogate for selftest\n", encoding="utf-8")
packet: dict[str, Any] = {
"schema": SCHEMA_ID,
"jurisdiction": "CN",
"not_legal_advice_ack": True,
"application": {
"software_full_name": "LightDemo 科研流程系统",
"version": "V1.0",
"copyright_owner": "Demo Owner",
"development_completion_date": "2026-07-01",
"first_publication_status": "未发表",
"development_mode": "独立开发",
},
"project_binding": {
"source_root_name": "LightDemo",
"manifest_sha256": "sha256:" + "1" * 64,
"source_files": [
{"id": "S1", "path": "src/app.py", "sha256": file_sha256(app_py)},
],
},
"source_plan": {
"schema": SOURCE_PLAN_SCHEMA,
"path": "source-deposit-plan.json",
"sha256": file_sha256(plan_file),
"confirmed": True,
"selected_file_count": 1,
"deposit_mode": "front_back_30",
"secret_scan_status": "PASS",
},
"rights_basis": {
"status": "VERIFIED",
"development_mode": "独立开发",
"basis": "User-confirmed independent development for selftest fixture.",
},
"third_party_code_review": {
"status": "PASS",
"summary": "No vendored, generated or unrelated third-party code is included in the selected source material.",
},
"stage_confirmations": {
key: {"confirmed": True, "confirmed_at": "2026-07-05", "basis": f"{key} confirmed by user"}
for key in CONFIRMATION_KEYS
},
"code_material": {
"source_type": "REAL_PROJECT",
"selection_confirmed": True,
"extraction_manifest_sha256": "sha256:" + "2" * 64,
"source_plan_sha256": file_sha256(plan_file),
"total_source_pages": 75,
"deposit_mode": "front_back_30",
"front_pages": 30,
"back_pages": 30,
"full_code_pages_have_min_50_lines": True,
"mid_file_snippet_used": False,
"exceptional_deposit": False,
},
"document_material": {
"total_document_pages": 30,
"deposit_mode": "all_document",
"all_document_included": True,
"full_document_pages_have_min_30_lines": True,
"business_context_confirmed": True,
},
"version_consistency": {
"all_materials_match_application": True,
"materials": [
{"locator": "申请表信息.txt", "version": "V1.0"},
{"locator": "LightDemo_操作手册.docx", "version": "V1.0"},
{"locator": "LightDemo-代码材料.docx", "version": "V1.0"},
],
},
"secret_scan": {"status": "PASS"},
"screenshots": {"required": False, "skip_reason": "Project has no GUI; manual uses command-line workflow."},
"submission": {"submit_to_office": False},
"formal_output": {
"generated": True,
"files": [
{"id": "O1", "path": "软件著作权申请资料/正式资料/LightDemo_操作手册.docx", "sha256": file_sha256(manual)},
{"id": "O2", "path": "软件著作权申请资料/正式资料/LightDemo-代码材料.docx", "sha256": file_sha256(code)},
{"id": "O3", "path": "软件著作权申请资料/正式资料/申请表信息.txt", "sha256": file_sha256(form)},
],
},
}
checks: list[tuple[bool, str]] = []
checks.append((validate(packet, tmp, dt.date(2026, 7, 5))["verdict"] == "PASS", "valid CN materials packet passes"))
bad_version = copy.deepcopy(packet)
bad_version["version_consistency"]["materials"][1]["version"] = "V2.0"
checks.append((validate(bad_version, tmp, dt.date(2026, 7, 5))["verdict"] == "FAIL", "version mismatch fails"))
fake_code = copy.deepcopy(packet)
fake_code["code_material"]["source_type"] = "AI_GENERATED"
checks.append((validate(fake_code, tmp, dt.date(2026, 7, 5))["verdict"] == "FAIL", "AI/fake code source fails"))
bad_pages = copy.deepcopy(packet)
bad_pages["code_material"]["back_pages"] = 12
checks.append((validate(bad_pages, tmp, dt.date(2026, 7, 5))["verdict"] == "FAIL", "front/back page rule fails"))
secret_found = copy.deepcopy(packet)
secret_found["secret_scan"]["status"] = "FOUND"
checks.append((validate(secret_found, tmp, dt.date(2026, 7, 5))["verdict"] == "FAIL", "secret scan failure blocks export"))
unverified_rights = copy.deepcopy(packet)
unverified_rights["rights_basis"]["status"] = "UNKNOWN"
checks.append((validate(unverified_rights, tmp, dt.date(2026, 7, 5))["verdict"] == "FAIL", "unverified rights basis blocks export"))
ok = True
for passed, label in checks:
ok &= passed
print(f" [{'OK' if passed else 'FAIL'}] {label}")
return 0 if ok else 1
finally:
shutil.rmtree(tmp, ignore_errors=True)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Validate Light software copyright material packet")
parser.add_argument("--packet", type=pathlib.Path, help="materials packet JSON")
parser.add_argument("--base", type=pathlib.Path, help="project root for source/hash checks")
parser.add_argument("--as-of", default=dt.date.today().isoformat(), help="validation date, YYYY-MM-DD")
parser.add_argument("--report", type=pathlib.Path, help="optional output report JSON")
parser.add_argument("--selftest", action="store_true", help="run built-in tests")
args = parser.parse_args(argv)
if args.selftest:
return run_selftest()
if args.packet is None:
parser.error("--packet is required unless --selftest is used")
date_problems: list[dict[str, str]] = []
as_of = parse_date(args.as_of, "--as-of", dt.date.max, date_problems) or dt.date.today()
packet = load_json(args.packet)
if not isinstance(packet, dict):
raise SystemExit("packet must be a JSON object")
report = validate(packet, args.base.resolve() if args.base else None, as_of)
if date_problems:
report["issues"] = date_problems + report["issues"]
report["verdict"] = "FAIL"
text = json.dumps(report, ensure_ascii=False, indent=2)
print(text)
if args.report:
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(text + "\n", encoding="utf-8")
return 0 if report["verdict"] == "PASS" else 1
if __name__ == "__main__":
raise SystemExit(main())
scripts/source_deposit_plan.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Create a source-code deposit plan for China software copyright materials.
The script inventories real project source files, estimates page mode, computes
SHA-256 hashes and performs a lightweight secret scan. It does not fabricate or
extract source material; the model/user must confirm the final selection before
formal export.
"""
from __future__ import annotations
import argparse
import datetime as dt
import hashlib
import json
import math
import pathlib
import re
import shutil
import sys
import tempfile
from typing import Any
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
SCHEMA = "light.software_copyright.source_deposit_plan.v1"
LINES_PER_PAGE = 50
SPLIT_THRESHOLD_PAGES = 60
MAX_FILE_BYTES = 900_000
CODE_EXTS = {
".py", ".js", ".jsx", ".ts", ".tsx", ".vue", ".svelte", ".java", ".kt", ".go",
".rs", ".c", ".cc", ".cpp", ".h", ".hpp", ".cs", ".php", ".rb", ".swift",
".m", ".mm", ".dart", ".scala", ".sql", ".html", ".css", ".scss", ".less",
".xml", ".json", ".yaml", ".yml", ".toml", ".ini", ".sh", ".ps1", ".bat",
}
SKIP_DIRS = {
".git", ".hg", ".svn", "node_modules", "dist", "build", ".next", ".nuxt",
"coverage", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache",
".venv", "venv", "env", "vendor", "third_party", "软件著作权申请资料",
}
SKIP_FILES = {
"package-lock.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock",
"pipfile.lock", "cargo.lock", "go.sum",
}
SECRET_PATTERNS = [
("aws_access_key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")),
("private_key", re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----")),
("api_key_assignment", re.compile(r"(?i)\b(api[_-]?key|secret|token|password|passwd|pwd)\b\s*[:=]\s*['\"][^'\"]{8,}['\"]")),
("connection_string", re.compile(r"(?i)\b(mysql|postgres|mongodb|redis)://[^@\s]+:[^@\s]+@")),
]
def repo_root(start: str | pathlib.Path | None = None) -> pathlib.Path:
cur = pathlib.Path(start or __file__).resolve()
if cur.is_file():
cur = cur.parent
while cur != cur.parent and not (cur / "skills").is_dir():
cur = cur.parent
if not (cur / "skills").is_dir():
raise RuntimeError("Light-Skills repository root not found")
return cur
def file_sha256(path: pathlib.Path) -> str:
h = hashlib.sha256()
with path.open("rb") as fh:
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
h.update(chunk)
return "sha256:" + h.hexdigest()
def rel(path: pathlib.Path, root: pathlib.Path) -> str:
return path.resolve().relative_to(root.resolve()).as_posix()
def looks_binary(path: pathlib.Path) -> bool:
try:
chunk = path.read_bytes()[:4096]
except OSError:
return True
return b"\x00" in chunk
def material_lines(text: str) -> list[str]:
return [line for line in text.splitlines() if line.strip()]
def priority_for(path: pathlib.Path, root: pathlib.Path) -> tuple[int, str]:
r = rel(path, root).lower()
name = path.name.lower()
if name in {"main.ts", "main.js", "main.tsx", "main.jsx", "app.vue", "app.tsx", "app.py"}:
return 0, "entry"
if "/router/" in r or "/routes/" in r or "router." in r or "routes." in r:
return 10, "route"
if "/pages/" in r or "/views/" in r or "/screens/" in r or "/app/" in r:
return 20, "page"
if "/api/" in r or "/apis/" in r or "/services/" in r or "request." in r:
return 30, "api-service"
if "/store/" in r or "/stores/" in r or "/redux/" in r or "/pinia/" in r:
return 40, "state"
if "/components/" in r:
return 50, "component"
if "/utils/" in r or "/lib/" in r or "/hooks/" in r or "/composables/" in r:
return 60, "utility"
if "/server/" in r or "/models/" in r or "/schemas/" in r or "/workers/" in r:
return 70, "backend-core"
if path.suffix.lower() in {".css", ".scss", ".less"}:
return 90, "style"
return 80, "source"
def should_skip(path: pathlib.Path, root: pathlib.Path) -> bool:
parts = set(path.resolve().relative_to(root.resolve()).parts)
if parts & SKIP_DIRS:
return True
if path.name.lower() in SKIP_FILES:
return True
if path.suffix.lower() not in CODE_EXTS:
return True
try:
size = path.stat().st_size
except OSError:
return True
if size <= 0 or size > MAX_FILE_BYTES:
return True
return looks_binary(path)
def iter_source_files(root: pathlib.Path) -> list[pathlib.Path]:
files: list[pathlib.Path] = []
for path in root.rglob("*"):
if not path.is_file():
continue
try:
if should_skip(path, root):
continue
except ValueError:
continue
files.append(path)
files.sort(key=lambda p: (*priority_for(p, root), rel(p, root)))
return files
def secret_findings(path: pathlib.Path, root: pathlib.Path, text: str) -> list[dict[str, Any]]:
findings: list[dict[str, Any]] = []
for line_no, line in enumerate(text.splitlines(), start=1):
for kind, pattern in SECRET_PATTERNS:
if pattern.search(line):
findings.append({"path": rel(path, root), "line": line_no, "kind": kind})
return findings
def build_plan(root: pathlib.Path, software_name: str | None, version: str | None, max_files: int) -> dict[str, Any]:
root = root.resolve()
if not root.is_dir():
raise SystemExit(f"project root not found: {root}")
candidates: list[dict[str, Any]] = []
findings: list[dict[str, Any]] = []
for path in iter_source_files(root):
try:
text = path.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
lines = text.splitlines()
nonblank = material_lines(text)
priority, tier = priority_for(path, root)
item = {
"path": rel(path, root),
"sha256": file_sha256(path),
"line_count": len(lines),
"material_line_count": len(nonblank),
"estimated_pages": math.ceil(max(1, len(nonblank)) / LINES_PER_PAGE),
"priority": priority,
"selection_tier": tier,
"recommended": False,
"model_reason": "",
}
candidates.append(item)
findings.extend(secret_findings(path, root, text))
if max_files and len(candidates) >= max_files:
break
recommended_lines = 0
recommended_count = 0
for item in candidates:
if item["material_line_count"] <= 0:
continue
item["recommended"] = True
item["model_reason"] = f"候选 {item['selection_tier']} 源码,可体现软件真实功能或运行逻辑;需用户确认。"
recommended_lines += int(item["material_line_count"]) + 1
recommended_count += 1
if math.ceil(recommended_lines / LINES_PER_PAGE) >= SPLIT_THRESHOLD_PAGES:
break
total_lines = sum(int(item["material_line_count"]) + 1 for item in candidates if item["material_line_count"] > 0)
recommended_pages = math.ceil(recommended_lines / LINES_PER_PAGE) if recommended_lines else 0
all_pages = math.ceil(total_lines / LINES_PER_PAGE) if total_lines else 0
deposit_mode = "front_back_30" if recommended_pages >= SPLIT_THRESHOLD_PAGES else "all_source"
secret_status = "FOUND" if findings else "PASS"
return {
"schema": SCHEMA,
"generated_at": dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat(),
"project_root_name": root.name,
"software_name": software_name or "",
"version": version or "",
"line_policy": {
"source_lines_per_page": LINES_PER_PAGE,
"split_threshold_pages": SPLIT_THRESHOLD_PAGES,
},
"source_summary": {
"candidate_file_count": len(candidates),
"all_candidate_material_lines": total_lines,
"all_candidate_estimated_pages": all_pages,
"recommended_file_count": recommended_count,
"recommended_material_lines": recommended_lines,
"recommended_estimated_pages": recommended_pages,
"deposit_mode": deposit_mode,
},
"secret_scan": {
"status": secret_status,
"finding_count": len(findings),
"findings": findings[:100],
"truncated": len(findings) > 100,
},
"candidate_files": candidates,
"selection_required": True,
"confirmation_required": True,
"confirmed": False,
"next_action": (
"由模型阅读业务理解与候选源码,必要时调整 recommended/model_reason;"
"用户确认后,将确认记录和本计划 SHA 写入 materials packet。"
),
}
def write_json(path: pathlib.Path, data: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def run_selftest() -> int:
root = repo_root()
e2e_root = root / ".upgrade" / "_e2e" / "software-copyright-source-plan"
e2e_root.mkdir(parents=True, exist_ok=True)
tmp = pathlib.Path(tempfile.mkdtemp(prefix="selftest-", dir=e2e_root))
try:
clean = tmp / "clean-project"
(clean / "src" / "app").mkdir(parents=True)
(clean / "src" / "app" / "page.tsx").write_text("\n".join(f"export const row{i} = {i};" for i in range(120)), encoding="utf-8")
(clean / "src" / "services").mkdir()
(clean / "src" / "services" / "api.ts").write_text("\n".join(f"export function api{i}() {{ return {i}; }}" for i in range(80)), encoding="utf-8")
(clean / "node_modules").mkdir()
(clean / "node_modules" / "ignored.js").write_text("should not be scanned\n", encoding="utf-8")
clean_plan = build_plan(clean, "LightDemo", "V1.0", 0)
out = tmp / "plan.json"
write_json(out, clean_plan)
secret = tmp / "secret-project"
(secret / "src").mkdir(parents=True)
(secret / "src" / "main.py").write_text('api_key = "sk-test-secret-value"\nprint(api_key)\n', encoding="utf-8")
secret_plan = build_plan(secret, "SecretDemo", "V1.0", 0)
checks = [
(clean_plan["schema"] == SCHEMA, "schema emitted"),
(clean_plan["secret_scan"]["status"] == "PASS", "clean project passes secret scan"),
(clean_plan["source_summary"]["candidate_file_count"] == 2, "dependency/build directories skipped"),
(clean_plan["source_summary"]["recommended_file_count"] > 0, "recommended source candidates produced"),
(out.is_file() and out.stat().st_size > 0, "plan JSON written"),
(secret_plan["secret_scan"]["status"] == "FOUND", "secret-like assignment is detected"),
]
ok = True
for passed, label in checks:
ok &= passed
print(f" [{'OK' if passed else 'FAIL'}] {label}")
return 0 if ok else 1
finally:
shutil.rmtree(tmp, ignore_errors=True)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Create a Light software-copyright source deposit plan")
parser.add_argument("--root", type=pathlib.Path, help="project root to scan")
parser.add_argument("--out", type=pathlib.Path, help="output JSON path")
parser.add_argument("--software-name", default="", help="confirmed or candidate software name")
parser.add_argument("--version", default="", help="confirmed or candidate version")
parser.add_argument("--max-files", type=int, default=0, help="limit candidate inventory for very large projects")
parser.add_argument("--selftest", action="store_true", help="run built-in tests")
args = parser.parse_args(argv)
if args.selftest:
return run_selftest()
if args.root is None:
parser.error("--root is required unless --selftest is used")
plan = build_plan(args.root, args.software_name, args.version, args.max_files)
text = json.dumps(plan, ensure_ascii=False, indent=2)
if args.out:
write_json(args.out, plan)
print(f"OK source deposit plan: {args.out}")
print(f"sha256: {file_sha256(args.out)}")
else:
print(text)
return 0
if __name__ == "__main__":
raise SystemExit(main())
SKILL.md
---
name: light-software-copyright
description: >-
Prepare China software copyright registration material drafts from a real
software project: application-field worksheet, source-code deposit material,
user/operation manual, screenshots when needed, consistency checks and local
formal-output evidence. Use for 软著, 软件著作权, 软件版权登记, source-code page
extraction, operation manual drafts, version/name consistency, or pre-submit
review. This off-DAG engineering/IP handoff skill does not provide legal
advice, does not submit applications, does not guarantee registration, and
never fabricates source code.
---
# Software copyright materials
Prepare auditable China software copyright material drafts from a real project.
The core job is not “write pretty documents”; it is to keep software name,
version, source-code material, manual, screenshots, applicant fields and user
confirmations consistent enough for the user or professional service provider
to review.
Read
[`references/software-copyright-resource-map.md`](references/software-copyright-resource-map.md)
before current-rule or material-format work.
Read
[`references/materials-workflow.md`](references/materials-workflow.md)
before source-code selection, business-understanding, rights-basis,
third-party-code or formal-output work.
## Non-negotiable boundaries
1. This skill is not legal advice and never submits applications or claims the
material will pass. It prepares local drafts and review evidence only.
2. Code material must come from real project source files. Never invent,
rewrite, pad, or AI-generate source code for deposit.
3. Do not expose secrets. Run or request a secret/privacy scan before exporting
source-code material; if secrets are present, stop for redaction strategy.
4. Preserve official-rule uncertainty. If the current China Copyright
Protection Center workflow, form, fee, page format or exception rule is not
verified that day, write `UNKNOWN` or `PLANNED`.
5. Keep every formal material version and software name consistent with the
application worksheet.
6. Keep this skill off the research DAG. It emits no `light.findings.v1`, has
no stage, no checkpoint gate and no reroute.
## Workflow
### 1. Intake and scope
Capture or mark `UNKNOWN`:
- jurisdiction: default `CN`; do not silently reuse CN rules for US/EU work;
- software full name, abbreviation if any, version, owner/applicant, developer,
completion date, publication status and development mode;
- whether the project is independent, commissioned, cooperative, inherited,
derivative or based on licensed third-party code;
- target output language, local folder for drafts, and whether screenshots are
needed.
Stop for user confirmation before formal documents are generated.
### 2. Bind to a real source snapshot and deposit plan
Inventory source files from the project the user placed in scope. Record
relative path, SHA-256 and why selected. Exclude:
- generated build artifacts, minified bundles, vendored dependencies and lock
files unless the user explicitly says they are the software expression to
deposit;
- secrets, tokens, keys, credentials, private data and unrelated third-party
code;
- arbitrary middle snippets chosen only to make the material look longer.
If the project is too small, submit all real source rather than padding.
Run the lightweight planner before extraction:
```bash
python scripts/source_deposit_plan.py --root <project-root> --out 软件著作权申请资料/草稿/source-deposit-plan.json --software-name "<软件全称>" --version "<版本号>"
python scripts/source_deposit_plan.py --selftest
```
Use the plan to confirm candidate files, page mode, selected file count,
secret-scan status and the plan SHA. If secret-like content is found, stop for
redaction before exporting code material.
### 3. Confirm application fields and business understanding
Generate a worksheet for the user to verify:
- software name and version;
- owner/applicant and contributor facts;
- development environment and operating environment;
- main functions, technical characteristics and use scenario;
- source program quantity and document material quantity;
- publication status and dates.
The manual must describe how a reviewer/user operates the software. It should
not be a generic feature list disconnected from screens, commands or workflows.
Record rights basis and third-party/open-source/generated-code review before
formal export. Do not assume independent ownership when the project may be
commissioned, cooperative, inherited, derivative or license-constrained.
### 4. Prepare code and document material
For normal China material logic, follow the official first/last rule:
- source program and one documentation material use front/back continuous 30
pages when the whole material is at least 60 pages;
- if the whole material is fewer than 60 pages, include the whole source or
whole document;
- full source pages normally need at least 50 lines, and full document pages at
least 30 lines, unless a documented official exception applies.
Keep extraction manifests and page counts. If exceptional deposit is requested,
mark it explicitly and ask the user to verify the current official rule.
### 5. Generate local drafts, then formal outputs
Use a local folder such as `软件著作权申请资料/`:
- `草稿/`: application worksheet, manual draft, code-selection notes;
- `正式资料/`: final DOCX/TXT/PDF-like outputs only after confirmation;
- manifest/report JSON: file paths, SHA-256, source snapshot, confirmation
basis and rule status.
Screenshots are optional; if not used, record why. If used, bind each screenshot
to a real running UI or user-provided image and hash it.
### 6. Run the machine gate before saying “ready for review”
Create a packet following
[`templates/software-copyright-materials.example.json`](templates/software-copyright-materials.example.json),
then run:
```bash
python scripts/source_deposit_plan.py --selftest
python scripts/materials_gate.py --packet software-copyright-materials.json --base <project-root> --as-of 2026-07-05
python scripts/materials_gate.py --selftest
```
The gate must pass before calling the package ready for user/professional
review. A failed gate means repair the source plan, source binding, rights
basis, third-party review, confirmation, page rule, version consistency, secret
handling or output hashes.
## ACT / ASK / NEVER
ACT:
- use real source files and record SHA-256;
- ask the user to confirm application fields, business context, code selection,
drafts and final export;
- keep source-code and documentation page counts explicit;
- make software name and version identical across all materials;
- scan for secrets/private data before exporting source-code material.
- record rights basis and third-party/open-source/generated-code exclusions.
ASK:
- owner/applicant and development-mode facts;
- whether third-party/commissioned/inherited/derivative code is present;
- whether screenshots should be included;
- whether exceptional deposit or redaction is needed;
- whether the software is independently developed, commissioned, cooperative,
inherited, derivative, or includes third-party/generated code;
- whether to stop at drafts or generate formal local files.
NEVER:
- submit materials or interact with the registration system for the user;
- promise approval, certificate timing, or “包过”;
- fabricate source code, pad line counts, or select arbitrary snippets;
- leak credentials, private data or unrelated third-party code;
- silently include vendored, generated or license-constrained code without a
third-party review note;
- route this skill into the Light research DAG.
templates/software-copyright-materials.example.json
{
"schema": "light.software_copyright.materials.v1",
"jurisdiction": "CN",
"not_legal_advice_ack": true,
"application": {
"software_full_name": "Example Software",
"version": "V1.0",
"copyright_owner": "Example Owner",
"development_completion_date": "2026-07-05",
"first_publication_status": "未发表",
"development_mode": "独立开发"
},
"project_binding": {
"source_root_name": "example-project",
"manifest_sha256": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"source_files": [
{
"id": "S1",
"path": "src/app.py",
"sha256": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
}
]
},
"source_plan": {
"schema": "light.software_copyright.source_deposit_plan.v1",
"path": "软件著作权申请资料/草稿/source-deposit-plan.json",
"sha256": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"confirmed": true,
"selected_file_count": 1,
"deposit_mode": "front_back_30",
"secret_scan_status": "PASS"
},
"rights_basis": {
"status": "VERIFIED",
"development_mode": "独立开发",
"basis": "Record user-confirmed ownership/acquisition basis and supporting document locator."
},
"third_party_code_review": {
"status": "PASS",
"summary": "Record whether vendored/generated/unrelated third-party code was excluded from the selected material.",
"excluded_paths": []
},
"stage_confirmations": {
"environment": {"confirmed": true, "confirmed_at": "2026-07-05", "basis": "user confirmed"},
"source_plan": {"confirmed": true, "confirmed_at": "2026-07-05", "basis": "user confirmed source-deposit plan"},
"application_fields": {"confirmed": true, "confirmed_at": "2026-07-05", "basis": "user confirmed"},
"business_context": {"confirmed": true, "confirmed_at": "2026-07-05", "basis": "user confirmed"},
"code_selection": {"confirmed": true, "confirmed_at": "2026-07-05", "basis": "user confirmed"},
"markdown_draft": {"confirmed": true, "confirmed_at": "2026-07-05", "basis": "user confirmed"},
"final_export": {"confirmed": true, "confirmed_at": "2026-07-05", "basis": "user confirmed"}
},
"code_material": {
"source_type": "REAL_PROJECT",
"selection_confirmed": true,
"extraction_manifest_sha256": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"source_plan_sha256": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"total_source_pages": 75,
"deposit_mode": "front_back_30",
"front_pages": 30,
"back_pages": 30,
"full_code_pages_have_min_50_lines": true,
"mid_file_snippet_used": false,
"exceptional_deposit": false
},
"document_material": {
"total_document_pages": 30,
"deposit_mode": "all_document",
"all_document_included": true,
"full_document_pages_have_min_30_lines": true,
"business_context_confirmed": true
},
"version_consistency": {
"all_materials_match_application": true,
"materials": [
{"locator": "申请表信息.txt", "version": "V1.0"},
{"locator": "操作手册.docx", "version": "V1.0"},
{"locator": "代码材料.docx", "version": "V1.0"}
]
},
"secret_scan": {"status": "PASS"},
"screenshots": {
"required": false,
"skip_reason": "No GUI screenshots are required for this project."
},
"submission": {"submit_to_office": false},
"formal_output": {
"generated": true,
"files": [
{
"id": "O1",
"path": "软件著作权申请资料/正式资料/申请表信息.txt",
"sha256": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
}
]
}
}