@tencent-rtc/trtc-agent-skills 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/README.zh.md +173 -0
- package/bin/cli.js +434 -0
- package/knowledge-base/index.yaml +454 -0
- package/knowledge-base/platform-slice-template.md +233 -0
- package/knowledge-base/scenario-spec.md +350 -0
- package/knowledge-base/scenarios/conference/base/general-conference.md +365 -0
- package/knowledge-base/scenarios/conference/base/webinar-conference.md +130 -0
- package/knowledge-base/scenarios/conference/medical/1v1-video-consultation.md +145 -0
- package/knowledge-base/scenarios/conference/medical/medical-multidoctor-consultation.md +113 -0
- package/knowledge-base/scenarios/live/entertainment-live-room.md +118 -0
- package/knowledge-base/slice-spec.md +546 -0
- package/knowledge-base/slices/conference/web/ai-tools.md +225 -0
- package/knowledge-base/slices/conference/web/beauty-effects.md +188 -0
- package/knowledge-base/slices/conference/web/device-control.md +338 -0
- package/knowledge-base/slices/conference/web/login-auth.md +261 -0
- package/knowledge-base/slices/conference/web/network-quality.md +190 -0
- package/knowledge-base/slices/conference/web/official-roomkit-api.md +298 -0
- package/knowledge-base/slices/conference/web/official-roomkit-login-ui.md +246 -0
- package/knowledge-base/slices/conference/web/participant-list.md +238 -0
- package/knowledge-base/slices/conference/web/participant-management.md +718 -0
- package/knowledge-base/slices/conference/web/prejoin-check.md +293 -0
- package/knowledge-base/slices/conference/web/room-call.md +213 -0
- package/knowledge-base/slices/conference/web/room-chat.md +426 -0
- package/knowledge-base/slices/conference/web/room-lifecycle.md +534 -0
- package/knowledge-base/slices/conference/web/room-schedule.md +281 -0
- package/knowledge-base/slices/conference/web/screen-share.md +211 -0
- package/knowledge-base/slices/conference/web/video-layout.md +675 -0
- package/knowledge-base/slices/conference/web/virtual-background.md +197 -0
- package/knowledge-base/slices/conference/web/webinar-interaction.md +206 -0
- package/knowledge-base/slices/live/anchor-lifecycle.md +122 -0
- package/knowledge-base/slices/live/anchor-preview.md +90 -0
- package/knowledge-base/slices/live/anchor-room-config.md +104 -0
- package/knowledge-base/slices/live/audience-list.md +86 -0
- package/knowledge-base/slices/live/audience-manage.md +92 -0
- package/knowledge-base/slices/live/audience-watch.md +85 -0
- package/knowledge-base/slices/live/audio.md +116 -0
- package/knowledge-base/slices/live/barrage.md +88 -0
- package/knowledge-base/slices/live/beauty.md +99 -0
- package/knowledge-base/slices/live/coguest-apply.md +105 -0
- package/knowledge-base/slices/live/device-control.md +91 -0
- package/knowledge-base/slices/live/error-codes.md +167 -0
- package/knowledge-base/slices/live/gift.md +84 -0
- package/knowledge-base/slices/live/ios/.gitkeep +0 -0
- package/knowledge-base/slices/live/ios/anchor-lifecycle.md +313 -0
- package/knowledge-base/slices/live/ios/anchor-preview.md +228 -0
- package/knowledge-base/slices/live/ios/anchor-room-config.md +257 -0
- package/knowledge-base/slices/live/ios/audience-list.md +353 -0
- package/knowledge-base/slices/live/ios/audience-manage.md +381 -0
- package/knowledge-base/slices/live/ios/audience-watch.md +286 -0
- package/knowledge-base/slices/live/ios/audio.md +373 -0
- package/knowledge-base/slices/live/ios/barrage.md +285 -0
- package/knowledge-base/slices/live/ios/beauty.md +323 -0
- package/knowledge-base/slices/live/ios/coguest-apply.md +506 -0
- package/knowledge-base/slices/live/ios/device-control.md +286 -0
- package/knowledge-base/slices/live/ios/error-codes.md +270 -0
- package/knowledge-base/slices/live/ios/gift.md +315 -0
- package/knowledge-base/slices/live/ios/live-list.md +269 -0
- package/knowledge-base/slices/live/ios/login-auth.md +247 -0
- package/knowledge-base/slices/live/live-list.md +82 -0
- package/knowledge-base/slices/live/login-auth.md +78 -0
- package/package.json +34 -0
- package/skills/trtc/SKILL.md +326 -0
- package/skills/trtc/room-builder/SKILL.md +138 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/README.md +108 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/backend-contract.zh-CN.md +162 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/integration.zh-CN.md +154 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/theme.zh-CN.md +78 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/index.html +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/package.json +28 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/postcss.config.js +5 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/App.vue +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/ConsultationManagePanel.vue +838 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LanguageSwitch.vue +102 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LoadingSpinner.vue +6 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalAlert.vue +34 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalBusinessPanel.vue +148 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalButton.vue +49 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalConfirmDialog.vue +68 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalDataPanel.vue +196 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalRecordPanel.vue +270 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/PrescriptionPanel.vue +363 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/basic-info-config.ts +29 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.d.ts +4 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.js +2 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/runtime-config.ts +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/env.d.ts +32 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationChatPanel.vue +123 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationMembersPanel.vue +230 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationTranscriptionPanel.vue +135 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationVideoStage.vue +113 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/InviteDoctorDialog.vue +132 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/KickMemberConfirmDialog.vue +50 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/types.ts +77 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationChat.ts +97 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationDevices.ts +48 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationParticipants.ts +121 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationPermissions.ts +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/utils.ts +70 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/en-US/index.ts +553 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/index.ts +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/medicalTranslate.ts +85 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/state.ts +49 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/zh-CN/index.ts +463 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/main.ts +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/appointments.ts +96 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/users.ts +79 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/router/index.ts +63 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/index.ts +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/appointmentService.ts +77 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/authService.ts +38 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/launchContext.ts +31 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/userService.ts +35 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/appointmentService.ts +43 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/authService.ts +33 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/userService.ts +43 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/types.ts +135 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/shared/icons.ts +53 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/index.css +106 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/tailwind.css +3 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/theme.css +209 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/auth.ts +50 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/format.ts +24 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/navigation.ts +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/session.ts +28 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorConsultationView.vue +777 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorDashboardView.vue +678 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/LoginView.vue +441 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationFinishedView.vue +185 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationView.vue +1003 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientSelectDoctorView.vue +317 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientWaitingView.vue +454 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.json +21 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.node.json +8 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/vite.config.ts +17 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation//346/216/245/345/205/245/350/257/264/346/230/216.md +6 -0
- package/skills/trtc/room-builder/tools/render_ai_instructions.py +226 -0
- package/skills/trtc-apply/SKILL.md +97 -0
- package/skills/trtc-apply/guardrails/apply_lib/__init__.py +0 -0
- package/skills/trtc-apply/guardrails/apply_lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/trtc-apply/guardrails/apply_lib/__pycache__/rule_parser.cpython-313.pyc +0 -0
- package/skills/trtc-apply/guardrails/apply_lib/rule_parser.py +268 -0
- package/skills/trtc-docs/SKILL.md +207 -0
- package/skills/trtc-onboarding/SKILL.md +839 -0
- package/skills/trtc-onboarding/reference/path-a1-demo.md +103 -0
- package/skills/trtc-onboarding/reference/path-a2-integrate.md +693 -0
- package/skills/trtc-onboarding/reference/path-b-troubleshoot.md +115 -0
- package/skills/trtc-onboarding/reference/path-c-expand.md +43 -0
- package/skills/trtc-onboarding/reference/reporting-protocol.md +174 -0
- package/skills/trtc-onboarding/reference/supported-matrix.md +100 -0
- package/skills/trtc-onboarding/reference/usersig-handling.md +140 -0
- package/skills/trtc-search/SKILL.md +221 -0
- package/skills/trtc-topic/SKILL.md +638 -0
- package/skills/trtc-topic/guardrails/__pycache__/gate_slice_read.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/__pycache__/gate_slice_write.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/__pycache__/stop_require_apply_evidence.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/gate_slice_read.py +133 -0
- package/skills/trtc-topic/guardrails/gate_slice_write.py +169 -0
- package/skills/trtc-topic/guardrails/stop_require_apply_evidence.py +97 -0
- package/skills/trtc-topic/references/execution-units.yaml +58 -0
- package/skills/trtc-topic/runtime/README.md +50 -0
- package/skills/trtc-topic/runtime/RUNTIME.md +128 -0
- package/skills/trtc-topic/runtime/lib/__init__.py +0 -0
- package/skills/trtc-topic/runtime/lib/platforms.py +194 -0
- package/skills/trtc-topic/runtime/package-lock.json +1211 -0
- package/skills/trtc-topic/runtime/package.json +13 -0
- package/skills/trtc-topic/runtime/telemetry-bridge.mjs +339 -0
- package/skills/trtc-topic/runtime/telemetry_collector.py +293 -0
- package/skills/trtc-topic/scripts/STATE-MACHINE-GUIDE.md +186 -0
- package/skills/trtc-topic/scripts/__pycache__/apply.cpython-313.pyc +0 -0
- package/skills/trtc-topic/scripts/apply.py +581 -0
- package/skills/trtc-topic/scripts/finalize_session.py +113 -0
- package/skills/trtc-topic/scripts/init_slice_queue.py +96 -0
- package/skills/trtc-topic/scripts/lib/__pycache__/state_machine.cpython-313.pyc +0 -0
- package/skills/trtc-topic/scripts/lib/state_machine.py +328 -0
- package/skills/trtc-topic/scripts/next_slice.py +137 -0
- package/skills/trtc-topic/tests/README.md +70 -0
- package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/conftest.py +72 -0
- package/skills/trtc-topic/tests/test_apply_cli.py +480 -0
- package/skills/trtc-topic/tests/test_end_to_end.py +305 -0
- package/skills/trtc-topic/tests/test_finalize_session.py +51 -0
- package/skills/trtc-topic/tests/test_gates.py +316 -0
- package/skills/trtc-topic/tests/test_session_resolver.py +260 -0
- package/skills/trtc-topic/tests/test_state_machine.py +414 -0
- package/skills/trtc-topic/tests/test_stop_require_apply.py +99 -0
- package/skills/trtc-topic/tests/test_topic_skill_invariants.py +130 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""render_ai_instructions.py — Render ai-instructions/*.md to tool-specific entry files.
|
|
3
|
+
|
|
4
|
+
Sources: ai-instructions/*.md (single source of truth, human-edited)
|
|
5
|
+
Targets:
|
|
6
|
+
- AGENTS.md (for Codex / Aider / Cline / CodeBuddy)
|
|
7
|
+
- CLAUDE.md (between AI-INSTRUCTIONS markers)
|
|
8
|
+
- .cursor/rules/{name}.mdc (one per source file; with Cursor frontmatter)
|
|
9
|
+
|
|
10
|
+
Grown TDD-style; see tests/unit/test_render_ai_instructions.py.
|
|
11
|
+
"""
|
|
12
|
+
import argparse
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _sources(project_root, *, include_base=True):
|
|
18
|
+
"""Return sorted list of .md source files under ai-instructions/.
|
|
19
|
+
|
|
20
|
+
base.md is the fixed preamble for AGENTS.md (tells Codex to read CLAUDE.md).
|
|
21
|
+
When include_base=False it is excluded — used by CLAUDE.md and Cursor renders
|
|
22
|
+
which don't need a self-referential pointer.
|
|
23
|
+
"""
|
|
24
|
+
src_dir = project_root / "ai-instructions"
|
|
25
|
+
if not src_dir.exists():
|
|
26
|
+
return []
|
|
27
|
+
files = sorted(src_dir.glob("*.md"))
|
|
28
|
+
if not include_base:
|
|
29
|
+
files = [f for f in files if f.name != "base.md"]
|
|
30
|
+
return files
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Single source-of-truth banner. Reused across all derived targets so the
|
|
34
|
+
# message stays identical and contributors can grep for "DO NOT EDIT" to
|
|
35
|
+
# find every generated file.
|
|
36
|
+
_BANNER = (
|
|
37
|
+
"<!-- DO NOT EDIT — generated from ai-instructions/ by "
|
|
38
|
+
"skills/trtc/room-builder/tools/render_ai_instructions.py. "
|
|
39
|
+
"Edit the source markdown and re-run the renderer instead. -->"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _render_agents_md(project_root):
|
|
44
|
+
"""Regenerate AGENTS.md by concatenating all sources.
|
|
45
|
+
|
|
46
|
+
base.md is rendered first as a plain preamble (no H1 header) so Codex
|
|
47
|
+
sees the "read CLAUDE.md" instruction at the top. Remaining sources get
|
|
48
|
+
an H1 section header per file.
|
|
49
|
+
"""
|
|
50
|
+
base_path = project_root / "ai-instructions" / "base.md"
|
|
51
|
+
sources = _sources(project_root, include_base=False)
|
|
52
|
+
parts = [_BANNER + "\n"]
|
|
53
|
+
# Preamble from base.md (rendered without a # header — it has its own).
|
|
54
|
+
if base_path.exists():
|
|
55
|
+
parts.append(base_path.read_text().rstrip() + "\n")
|
|
56
|
+
for src in sources:
|
|
57
|
+
parts.append(f"# {src.stem}\n\n{src.read_text().rstrip()}\n")
|
|
58
|
+
(project_root / "AGENTS.md").write_text("\n".join(parts) if (base_path.exists() or sources) else _BANNER + "\n")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
BEGIN_MARKER = "<!-- AI-INSTRUCTIONS:BEGIN -->"
|
|
62
|
+
END_MARKER = "<!-- AI-INSTRUCTIONS:END -->"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _demote_headings(body):
|
|
66
|
+
"""Add one '#' to every ATX heading line.
|
|
67
|
+
|
|
68
|
+
`## Foo` → `### Foo`, `### Foo` → `#### Foo`, etc. Lines that aren't
|
|
69
|
+
headings are returned unchanged. Used in CLAUDE.md so body sections
|
|
70
|
+
nest under the renderer-prepended `## {name}` parent.
|
|
71
|
+
"""
|
|
72
|
+
out = []
|
|
73
|
+
for line in body.splitlines():
|
|
74
|
+
# Match an ATX heading: 1-6 '#' followed by a space.
|
|
75
|
+
stripped = line.lstrip()
|
|
76
|
+
if stripped.startswith("#"):
|
|
77
|
+
hashes = len(stripped) - len(stripped.lstrip("#"))
|
|
78
|
+
if 1 <= hashes <= 5 and stripped[hashes:hashes + 1] == " ":
|
|
79
|
+
# Preserve leading whitespace (rare in markdown but safe).
|
|
80
|
+
lead = line[:len(line) - len(stripped)]
|
|
81
|
+
out.append(f"{lead}#{stripped}")
|
|
82
|
+
continue
|
|
83
|
+
out.append(line)
|
|
84
|
+
return "\n".join(out)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _rendered_block(project_root):
|
|
88
|
+
"""The string content placed between markers in CLAUDE.md.
|
|
89
|
+
|
|
90
|
+
Section headers are H2 (`## {name}`). Body headings are demoted by one
|
|
91
|
+
level so they nest as children of the section header instead of
|
|
92
|
+
appearing as adjacent siblings. Banner placed first so a casual reader
|
|
93
|
+
sees the warning before any rendered content.
|
|
94
|
+
|
|
95
|
+
base.md is excluded — CLAUDE.md doesn't need a pointer to itself.
|
|
96
|
+
"""
|
|
97
|
+
sources = _sources(project_root, include_base=False)
|
|
98
|
+
parts = [_BANNER + "\n"]
|
|
99
|
+
for src in sources:
|
|
100
|
+
body = _demote_headings(src.read_text().rstrip())
|
|
101
|
+
parts.append(f"## {src.stem}\n\n{body}\n")
|
|
102
|
+
return "\n".join(parts)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _render_claude_md(project_root):
|
|
106
|
+
"""Update CLAUDE.md in place between AI-INSTRUCTIONS markers.
|
|
107
|
+
|
|
108
|
+
If markers exist: replace content strictly between them.
|
|
109
|
+
If markers don't exist: append markers + rendered block at EOF.
|
|
110
|
+
Content outside the markers is never touched.
|
|
111
|
+
"""
|
|
112
|
+
claude_path = project_root / "CLAUDE.md"
|
|
113
|
+
existing = claude_path.read_text() if claude_path.exists() else ""
|
|
114
|
+
block = _rendered_block(project_root)
|
|
115
|
+
marker_block = f"{BEGIN_MARKER}\n{block}\n{END_MARKER}\n"
|
|
116
|
+
|
|
117
|
+
if BEGIN_MARKER in existing and END_MARKER in existing:
|
|
118
|
+
begin = existing.index(BEGIN_MARKER)
|
|
119
|
+
end = existing.index(END_MARKER) + len(END_MARKER)
|
|
120
|
+
# Preserve trailing newline behavior from the original end-marker position.
|
|
121
|
+
trailing_nl = "\n" if end < len(existing) and existing[end] == "\n" else ""
|
|
122
|
+
new = existing[:begin] + marker_block.rstrip("\n") + trailing_nl + existing[end + len(trailing_nl):]
|
|
123
|
+
claude_path.write_text(new)
|
|
124
|
+
else:
|
|
125
|
+
# Append block at EOF (with a blank line separator).
|
|
126
|
+
sep = "" if existing.endswith("\n\n") or not existing else ("\n" if existing.endswith("\n") else "\n\n")
|
|
127
|
+
claude_path.write_text(existing + sep + marker_block)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _render_cursor_rules(project_root):
|
|
131
|
+
"""One .cursor/rules/{name}.mdc per source file, with Cursor frontmatter.
|
|
132
|
+
|
|
133
|
+
Banner placed AFTER the frontmatter so Cursor's YAML parser still works.
|
|
134
|
+
base.md is excluded — Cursor reads its own rules, no redirect needed.
|
|
135
|
+
"""
|
|
136
|
+
rules_dir = project_root / ".cursor" / "rules"
|
|
137
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
138
|
+
for src in _sources(project_root, include_base=False):
|
|
139
|
+
target = rules_dir / f"{src.stem}.mdc"
|
|
140
|
+
body = src.read_text().rstrip()
|
|
141
|
+
target.write_text(
|
|
142
|
+
"---\n"
|
|
143
|
+
"alwaysApply: true\n"
|
|
144
|
+
"---\n"
|
|
145
|
+
"\n"
|
|
146
|
+
f"{_BANNER}\n"
|
|
147
|
+
"\n"
|
|
148
|
+
f"{body}\n"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _check_targets_up_to_date(project_root):
|
|
153
|
+
"""Compare current target contents to what would be rendered.
|
|
154
|
+
|
|
155
|
+
Returns a list of stale target paths (relative to project_root). Empty
|
|
156
|
+
list = everything in sync.
|
|
157
|
+
|
|
158
|
+
Implementation: render to a temporary in-memory representation and diff
|
|
159
|
+
against current file contents. The cleanest way is to capture what
|
|
160
|
+
each render function would write before it writes; rather than
|
|
161
|
+
refactoring all three to return strings, we copy the current targets
|
|
162
|
+
out, run the renderers, diff, then restore on mismatch (so --check
|
|
163
|
+
has no side effect on the working tree).
|
|
164
|
+
"""
|
|
165
|
+
targets = ["AGENTS.md", "CLAUDE.md"]
|
|
166
|
+
rules_dir = project_root / ".cursor" / "rules"
|
|
167
|
+
for src in _sources(project_root):
|
|
168
|
+
targets.append(f".cursor/rules/{src.stem}.mdc")
|
|
169
|
+
|
|
170
|
+
# Snapshot current contents.
|
|
171
|
+
snapshot = {}
|
|
172
|
+
for rel in targets:
|
|
173
|
+
p = project_root / rel
|
|
174
|
+
snapshot[rel] = p.read_bytes() if p.exists() else None
|
|
175
|
+
|
|
176
|
+
# Run render to compute new contents.
|
|
177
|
+
_render_agents_md(project_root)
|
|
178
|
+
_render_claude_md(project_root)
|
|
179
|
+
_render_cursor_rules(project_root)
|
|
180
|
+
|
|
181
|
+
stale = []
|
|
182
|
+
for rel in targets:
|
|
183
|
+
p = project_root / rel
|
|
184
|
+
new = p.read_bytes() if p.exists() else None
|
|
185
|
+
if new != snapshot[rel]:
|
|
186
|
+
stale.append(rel)
|
|
187
|
+
|
|
188
|
+
# Restore original contents so --check has no side effect.
|
|
189
|
+
for rel, original in snapshot.items():
|
|
190
|
+
p = project_root / rel
|
|
191
|
+
if original is None:
|
|
192
|
+
if p.exists():
|
|
193
|
+
p.unlink()
|
|
194
|
+
else:
|
|
195
|
+
p.write_bytes(original)
|
|
196
|
+
return stale
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def main():
|
|
200
|
+
parser = argparse.ArgumentParser(description="Render ai-instructions/*.md to tool files.")
|
|
201
|
+
parser.add_argument("--project-root", default=".",
|
|
202
|
+
help="Repo root (defaults to CWD)")
|
|
203
|
+
parser.add_argument("--check", action="store_true",
|
|
204
|
+
help="Exit 2 if any target is stale (CI mode); no writes.")
|
|
205
|
+
args = parser.parse_args()
|
|
206
|
+
root = Path(args.project_root).resolve()
|
|
207
|
+
|
|
208
|
+
if args.check:
|
|
209
|
+
stale = _check_targets_up_to_date(root)
|
|
210
|
+
if stale:
|
|
211
|
+
print("render_ai_instructions: stale targets:", file=sys.stderr)
|
|
212
|
+
for s in stale:
|
|
213
|
+
print(f" {s}", file=sys.stderr)
|
|
214
|
+
print("Re-run `python3 skills/trtc/room-builder/tools/render_ai_instructions.py` and commit the diff.",
|
|
215
|
+
file=sys.stderr)
|
|
216
|
+
return 2
|
|
217
|
+
return 0
|
|
218
|
+
|
|
219
|
+
_render_agents_md(root)
|
|
220
|
+
_render_claude_md(root)
|
|
221
|
+
_render_cursor_rules(root)
|
|
222
|
+
return 0
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
sys.exit(main())
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trtc-apply
|
|
3
|
+
description: >
|
|
4
|
+
INTERNAL structural gate for TRTC code generated by the topic / onboarding
|
|
5
|
+
skills. Not a user-facing skill, and NOT a correctness verifier. It does one
|
|
6
|
+
job: stop the AI from declaring a slice "done" and ending the turn before a
|
|
7
|
+
deterministic check has run. The check itself is lightweight — code exists +
|
|
8
|
+
the slice's entry symbol is wired up (with comment/string anti-cheat) — it
|
|
9
|
+
does NOT verify types, compilation, or runtime behavior. Triggered only by
|
|
10
|
+
other skills in this repo (topic step gates, onboarding A2). Do NOT route to
|
|
11
|
+
this skill when a user asks "review my code" or "check this implementation".
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# TRTC apply — Structural Gate
|
|
15
|
+
|
|
16
|
+
apply 是 topic / onboarding 代码交付流程里的一道**结构门**,不是正确性校验器。
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
它保证「流程没被跳过」,不保证「代码是对的」。
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
正确性由两处负责,不在 apply 范围内:
|
|
23
|
+
- **生成阶段**:slice 的 MUST / MUST NOT 约束指导代码怎么写。
|
|
24
|
+
- **客户侧**:在真实项目里编译 / 运行确认。apply 跑在客户五花八门的已有项目里,无法假设一个统一可靠的 build,因此**刻意不做编译**——编译失败往往与生成代码无关(历史报错、缺依赖、私有 registry、monorepo 配置),把它当门禁只会产生噪声并拖慢每个 slice。
|
|
25
|
+
|
|
26
|
+
本 skill 只有一种触发场景:topic / onboarding 生成代码后**内部调用**。它不是面向外部用户的"贴代码帮我检查"服务;若用户贴代码求排障,走 onboarding Path B。
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 它实际做的两件事
|
|
31
|
+
|
|
32
|
+
### 1. 强制门(防止"自我宣布完成就停")
|
|
33
|
+
|
|
34
|
+
apply 接在状态机上。每个 slice 的状态流转:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
slice_read → code_written → apply_passed → user_confirmed
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- 写完代码后必须运行 `apply.py`,把状态从 `code_written` 推到 `apply_passed`,否则
|
|
41
|
+
- `guardrails/stop_require_apply_evidence.py`(Stop hook)会**拦住结束**:`code_written` 或 `apply_failed` 状态下不允许 AI 结束本轮。
|
|
42
|
+
|
|
43
|
+
这是 apply 真正的价值:LLM 不能写一段"看起来对"的代码就自我盖章完成。
|
|
44
|
+
|
|
45
|
+
### 2. 代码非空 + 入口符号检查(轻量,不等于正确)
|
|
46
|
+
|
|
47
|
+
`apply.py` 扫 `<project_root>/src/**/*.{vue,ts}`,做两件事:
|
|
48
|
+
|
|
49
|
+
- **代码非空**:`src/` 不存在或没有源码 → `static-only` → fail(没有可检查的内容,不放行)。
|
|
50
|
+
- **入口符号**:每个 slice 有一个「入口符号」(它的 composable / 组件,如 device-control 的 `useDeviceState`)。该入口符号作为**真实代码标识符**出现在某个源文件里即通过;没有出现则 fail;**该 slice 没有登记入口符号 → 跳过**(无法机械检查,绝不误判)。
|
|
51
|
+
|
|
52
|
+
入口符号映射是单一来源:`apply_lib/rule_parser.py` 的 `COMPOSABLE_TO_SLICE`(配 `entry_symbols_for_slice()`)。
|
|
53
|
+
|
|
54
|
+
- 匹配前先用 `_strip_comments_and_strings` 剥掉注释与字符串字面量——防止把入口塞进 `// 注释` 或 `"字符串"` 骗过检查(来自 demo-test-2 真实 bug)。**入口符号是代码标识符,从不出现在字符串里,所以这步剥离不会冤枉正确代码**(这正是它取代旧「MUST 符号 grep」的原因:旧检查会把写在字符串里的符号——如错误码常量——剥掉而产生假阴性)。
|
|
55
|
+
- 失败信息会点名该 slice 的入口 composable(那是它公开文档里的 import,不是隐藏的 API pattern,点名安全且有助修复)。
|
|
56
|
+
|
|
57
|
+
> 明确边界:这一步**不验证**参数 / 类型 / 调用顺序 / 是否被执行 / 能否编译 / 运行时行为,也**不再**逐条核对 slice 的 MUST 符号。它只回答"代码非空,且这个 slice 的能力入口被接上了"。正确性由 slice 的 MUST / MUST NOT 约束(生成阶段)和客户侧编译运行负责。
|
|
58
|
+
|
|
59
|
+
### 3. 重复声明检查(窄范围,真实编译错误)
|
|
60
|
+
|
|
61
|
+
`apply.py` 还有一个**高精度、窄范围**的编译安全检查:只针对"从 `use*()` 调用解构出来的名字"——同名被解构 ≥2 次,或被解构且又以 `const/function/class` 声明,则判为 `duplicate-declaration` 并 fail。两个真实案例:
|
|
62
|
+
|
|
63
|
+
- `const { getCameraList } = useDeviceState()` 之后又写 `function getCameraList()`;
|
|
64
|
+
- `subscribeEvent` 同时从 `useRoomParticipantState()` 和 `useRoomState()` 解构。
|
|
65
|
+
|
|
66
|
+
它**不是**通用重复声明 linter(两个不同作用域的同名局部 `const` 不会被误报)。修法是给其中一个解构起别名:`const { subscribeEvent: subscribeParticipantEvent } = useRoomParticipantState()`。
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 调用方式
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python3 skills/trtc-topic/scripts/apply.py --slice <slice_id>
|
|
74
|
+
# 或按交付单元
|
|
75
|
+
python3 skills/trtc-topic/scripts/apply.py --unit <unit_id>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- session 路径解析:`$TRTC_SESSION_PATH` → `$CLAUDE_PROJECT_DIR/.trtc-session.yaml` → `./.trtc-session.yaml`。
|
|
79
|
+
- 退出码:`0` 通过(状态 → `apply_passed`) / `1` 失败(状态 → `apply_failed`) / `2` 用法错误。
|
|
80
|
+
- 证据写入 `<session_dir>/.trtc-apply-evidence/<slug>.json`(状态、入口检查数 `entries_checked`、每个 slice 的 `entry_result`、未过项的语义文本)。
|
|
81
|
+
- `src/` 不存在或无源码 → 记为 `static-only`,结论为 `fail`(无可检查内容,不放行)。
|
|
82
|
+
|
|
83
|
+
`auto_advance_policy` 为 `pause_on_failure` / `pause_at_end` 时,通过后自动推进到 `user_confirmed`;否则(默认 `pause_each`)保留每步向用户确认的暂停。
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 范围与非目标
|
|
88
|
+
|
|
89
|
+
| 在范围内 | 不在范围内 |
|
|
90
|
+
|---------|-----------|
|
|
91
|
+
| web / vue 生成代码 | iOS / Android / Flutter / Electron |
|
|
92
|
+
| 代码非空 + 单 slice 入口符号检查 | 类型 / 编译 / 运行时正确性 |
|
|
93
|
+
| 注释/字符串防作弊 | 逐条核对 slice 的 MUST 符号 / 参数 / 调用顺序 |
|
|
94
|
+
| 重复声明(编译安全)检查 | 跨 slice 前置状态、生命周期、清理对称性 |
|
|
95
|
+
| 状态机强制门 + Stop hook | 集成安全(diff 范围、SDK 初始化冲突、回归测试) |
|
|
96
|
+
|
|
97
|
+
入口符号检查只在「共用入口」(如 `conference` 对象同时承载 login-auth 与 room-lifecycle)上区分度较弱:导入一次即可让同入口的多个 slice 都过。这是「删掉逐符号 grep、改用入口门」时已接受的取舍——门的核心价值是状态机强制(不许自我盖章结束),而非判据的精度。跨 slice / 场景级的更强校验目前未实现,且不应硬编码进本门;正确性应由 slice 的 MUST / MUST NOT 约束与客户侧编译运行承接。
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""rule_parser.py — Slice rule extraction + entry-symbol map.
|
|
2
|
+
|
|
3
|
+
Two responsibilities:
|
|
4
|
+
|
|
5
|
+
* ``extract_rules_from_slice`` / ``rules_for_file`` — parse the
|
|
6
|
+
"代码生成约束 / 生成规则" sections of slice .md files into structured
|
|
7
|
+
MUST/MUST NOT rules (still used for documentation/inspection).
|
|
8
|
+
* ``COMPOSABLE_TO_SLICE`` / ``entry_symbols_for_slice`` — the slice ↔ entry
|
|
9
|
+
symbol map. The structural gate (``skills/trtc-topic/scripts/apply.py``)
|
|
10
|
+
uses this to check that a slice's entry composable/component is wired up
|
|
11
|
+
in the generated code. (The gate no longer greps each rule's backtick
|
|
12
|
+
patterns — that produced false negatives on symbols living inside string
|
|
13
|
+
literals; see apply.py's module docstring.)
|
|
14
|
+
|
|
15
|
+
Each rule has:
|
|
16
|
+
- type: "MUST" or "MUST NOT"
|
|
17
|
+
- text: The full rule description
|
|
18
|
+
- patterns: List of grep-able code patterns extracted from backtick segments
|
|
19
|
+
- verify_hint: The "Verify:" instruction if present
|
|
20
|
+
- source_file: Path to the slice .md file
|
|
21
|
+
- slice_id: e.g. "conference/login-auth"
|
|
22
|
+
"""
|
|
23
|
+
import re
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Match numbered MUST/MUST NOT rules in slice files.
|
|
29
|
+
# Format: "N. **text** — explanation\n **Verify**: check"
|
|
30
|
+
_RULE_BLOCK_RE = re.compile(
|
|
31
|
+
r'^\d+\.\s+\*\*(.*?)\*\*\s*[—–-]\s*(.*?)$',
|
|
32
|
+
re.MULTILINE
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Match the section headers for MUST / MUST NOT
|
|
36
|
+
_MUST_SECTION_RE = re.compile(
|
|
37
|
+
r'^#{2,4}\s+MUST(?:\s+NOT)?\s*[((].*?[))]',
|
|
38
|
+
re.MULTILINE
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Extract code patterns from backtick-quoted segments
|
|
42
|
+
_BACKTICK_RE = re.compile(r'`([^`]+)`')
|
|
43
|
+
|
|
44
|
+
# Match "**Verify**:" lines
|
|
45
|
+
_VERIFY_RE = re.compile(r'\*\*Verify\*\*:\s*(.+?)$', re.MULTILINE)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Entry composable / component / value → slice_id.
|
|
49
|
+
#
|
|
50
|
+
# This is the single source for "what is this slice's entry symbol". It is
|
|
51
|
+
# consumed in two places:
|
|
52
|
+
# * rules_for_file() — to decide which slice's rules apply to a file.
|
|
53
|
+
# * the apply gate (apply.py) — to check that a slice's entry symbol
|
|
54
|
+
# actually appears in the generated code.
|
|
55
|
+
#
|
|
56
|
+
# Entry symbols are stable code identifiers (composables / components / exported
|
|
57
|
+
# values), never string literals — which is what makes the entry-presence check
|
|
58
|
+
# immune to the comment/string-literal stripping false-negative that the old
|
|
59
|
+
# MUST-symbol grep suffered from.
|
|
60
|
+
COMPOSABLE_TO_SLICE = {
|
|
61
|
+
'useLoginState': 'conference/login-auth',
|
|
62
|
+
'useRoomState': 'conference/room-lifecycle',
|
|
63
|
+
'useDeviceState': 'conference/device-control',
|
|
64
|
+
'useRoomParticipantState': 'conference/participant-list',
|
|
65
|
+
'RoomView': 'conference/video-layout',
|
|
66
|
+
'RoomLayoutTemplate': 'conference/video-layout',
|
|
67
|
+
'networkInfo': 'conference/network-quality',
|
|
68
|
+
'NetworkQuality': 'conference/network-quality',
|
|
69
|
+
'useConversationListState': 'conference/room-chat',
|
|
70
|
+
'useMessageListState': 'conference/room-chat',
|
|
71
|
+
'useMessageInputState': 'conference/room-chat',
|
|
72
|
+
'setActiveConversation': 'conference/room-chat',
|
|
73
|
+
'startScreenShare': 'conference/screen-share',
|
|
74
|
+
'stopScreenShare': 'conference/screen-share',
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def entry_symbols_for_slice(slice_id: str) -> list:
|
|
79
|
+
"""Return the known entry symbols for a slice (inverse of COMPOSABLE_TO_SLICE).
|
|
80
|
+
|
|
81
|
+
Empty list means the slice has no registered entry symbol; callers should
|
|
82
|
+
treat that as "cannot check mechanically" rather than a failure.
|
|
83
|
+
"""
|
|
84
|
+
return [sym for sym, sid in COMPOSABLE_TO_SLICE.items() if sid == slice_id]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def extract_rules_from_slice(md_path: Path) -> list:
|
|
88
|
+
"""Parse a slice markdown file for MUST/MUST NOT rules.
|
|
89
|
+
|
|
90
|
+
Looks for the "代码生成约束" / "生成规则" section and extracts
|
|
91
|
+
structured rules from the MUST/MUST NOT subsections.
|
|
92
|
+
|
|
93
|
+
Returns list of rule dicts.
|
|
94
|
+
"""
|
|
95
|
+
if not md_path.exists():
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
content = md_path.read_text(encoding='utf-8')
|
|
99
|
+
|
|
100
|
+
# Find the code generation constraints section
|
|
101
|
+
constraint_start = _find_constraint_section(content)
|
|
102
|
+
if constraint_start is None:
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
constraint_text = content[constraint_start:]
|
|
106
|
+
rules = []
|
|
107
|
+
|
|
108
|
+
# Split into MUST and MUST NOT sections
|
|
109
|
+
must_section = _extract_section(constraint_text, 'MUST(生成时必须包含)')
|
|
110
|
+
must_not_section = _extract_section(constraint_text, 'MUST NOT(生成时绝不能出现)')
|
|
111
|
+
|
|
112
|
+
if must_section:
|
|
113
|
+
rules.extend(_parse_rules_in_section(must_section, 'MUST', md_path))
|
|
114
|
+
if must_not_section:
|
|
115
|
+
rules.extend(_parse_rules_in_section(must_not_section, 'MUST NOT', md_path))
|
|
116
|
+
|
|
117
|
+
return rules
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _find_constraint_section(content: str) -> Optional[int]:
|
|
121
|
+
"""Find the start of the code generation constraints section."""
|
|
122
|
+
markers = [
|
|
123
|
+
'## 代码生成约束',
|
|
124
|
+
'### 生成规则',
|
|
125
|
+
'#### MUST(生成时必须包含)',
|
|
126
|
+
'#### MUST(',
|
|
127
|
+
]
|
|
128
|
+
for marker in markers:
|
|
129
|
+
idx = content.find(marker)
|
|
130
|
+
if idx != -1:
|
|
131
|
+
return idx
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _extract_section(text: str, header_fragment: str) -> Optional[str]:
|
|
136
|
+
"""Extract text from a section header to the next same-or-higher-level header."""
|
|
137
|
+
idx = text.find(header_fragment)
|
|
138
|
+
if idx == -1:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
# Find the end: next #### or ### or ## header
|
|
142
|
+
section_start = text.find('\n', idx) + 1
|
|
143
|
+
remaining = text[section_start:]
|
|
144
|
+
|
|
145
|
+
# End at next heading of same or higher level
|
|
146
|
+
end_match = re.search(r'^#{2,4}\s+', remaining, re.MULTILINE)
|
|
147
|
+
if end_match:
|
|
148
|
+
return remaining[:end_match.start()]
|
|
149
|
+
return remaining
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _parse_rules_in_section(section_text: str, rule_type: str, source_file: Path) -> list:
|
|
153
|
+
"""Parse numbered rules from a MUST or MUST NOT section."""
|
|
154
|
+
rules = []
|
|
155
|
+
|
|
156
|
+
# Split by numbered items (1. ... 2. ... 3. ...)
|
|
157
|
+
items = re.split(r'\n(?=\d+\.\s+)', section_text)
|
|
158
|
+
|
|
159
|
+
for item in items:
|
|
160
|
+
item = item.strip()
|
|
161
|
+
if not item or not re.match(r'^\d+\.', item):
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
# Extract the rule title (bold text after number)
|
|
165
|
+
title_match = re.match(r'^\d+\.\s+\*\*(.*?)\*\*', item)
|
|
166
|
+
if not title_match:
|
|
167
|
+
# Try without bold
|
|
168
|
+
title_match = re.match(r'^\d+\.\s+(.+?)(?:\s*[—–-]|$)', item)
|
|
169
|
+
|
|
170
|
+
title = title_match.group(1) if title_match else item[:80]
|
|
171
|
+
|
|
172
|
+
# Extract code patterns from backticks
|
|
173
|
+
patterns = _BACKTICK_RE.findall(item)
|
|
174
|
+
|
|
175
|
+
# Extract verify hint
|
|
176
|
+
verify_match = _VERIFY_RE.search(item)
|
|
177
|
+
verify_hint = verify_match.group(1) if verify_match else None
|
|
178
|
+
|
|
179
|
+
# Derive slice_id from file path
|
|
180
|
+
slice_id = _derive_slice_id(source_file)
|
|
181
|
+
|
|
182
|
+
rules.append({
|
|
183
|
+
'type': rule_type,
|
|
184
|
+
'text': title.strip(),
|
|
185
|
+
'patterns': patterns,
|
|
186
|
+
'verify_hint': verify_hint,
|
|
187
|
+
'source_file': str(source_file),
|
|
188
|
+
'slice_id': slice_id,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
return rules
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _derive_slice_id(md_path: Path) -> str:
|
|
195
|
+
"""Derive slice_id from file path.
|
|
196
|
+
|
|
197
|
+
e.g. .../slices/conference/web/login-auth.md → conference/login-auth
|
|
198
|
+
"""
|
|
199
|
+
parts = md_path.parts
|
|
200
|
+
try:
|
|
201
|
+
slices_idx = parts.index('slices')
|
|
202
|
+
# Pattern: slices/{product}/{platform}/{ability}.md
|
|
203
|
+
# or: slices/{product}/{ability}.md
|
|
204
|
+
remaining = parts[slices_idx + 1:]
|
|
205
|
+
if len(remaining) >= 3:
|
|
206
|
+
# slices/conference/web/login-auth.md
|
|
207
|
+
product = remaining[0]
|
|
208
|
+
ability = remaining[-1].replace('.md', '')
|
|
209
|
+
return f"{product}/{ability}"
|
|
210
|
+
elif len(remaining) == 2:
|
|
211
|
+
# slices/conference/login-auth.md
|
|
212
|
+
product = remaining[0]
|
|
213
|
+
ability = remaining[1].replace('.md', '')
|
|
214
|
+
return f"{product}/{ability}"
|
|
215
|
+
except (ValueError, IndexError):
|
|
216
|
+
pass
|
|
217
|
+
return str(md_path.stem)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def load_rules_for_product_platform(
|
|
221
|
+
kb_root: Path,
|
|
222
|
+
product: str,
|
|
223
|
+
platform: str,
|
|
224
|
+
) -> list:
|
|
225
|
+
"""Load all rules for a given product and platform.
|
|
226
|
+
|
|
227
|
+
Loads both product-level and platform-specific slice files.
|
|
228
|
+
"""
|
|
229
|
+
slices_dir = kb_root / 'knowledge-base' / 'slices' / product
|
|
230
|
+
all_rules = []
|
|
231
|
+
|
|
232
|
+
# Product-level slices
|
|
233
|
+
for md_file in slices_dir.glob('*.md'):
|
|
234
|
+
all_rules.extend(extract_rules_from_slice(md_file))
|
|
235
|
+
|
|
236
|
+
# Platform-specific slices
|
|
237
|
+
platform_dir = slices_dir / platform
|
|
238
|
+
if platform_dir.exists():
|
|
239
|
+
for md_file in platform_dir.glob('*.md'):
|
|
240
|
+
all_rules.extend(extract_rules_from_slice(md_file))
|
|
241
|
+
|
|
242
|
+
return all_rules
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def rules_for_file(all_rules: list, file_content: str) -> list:
|
|
246
|
+
"""Filter rules relevant to a specific file based on what it imports.
|
|
247
|
+
|
|
248
|
+
A rule is relevant if the file contains any composable/import that
|
|
249
|
+
the rule's slice defines.
|
|
250
|
+
"""
|
|
251
|
+
# Build a map: slice_id → rules
|
|
252
|
+
by_slice = {}
|
|
253
|
+
for rule in all_rules:
|
|
254
|
+
sid = rule.get('slice_id', '')
|
|
255
|
+
by_slice.setdefault(sid, []).append(rule)
|
|
256
|
+
|
|
257
|
+
# Determine which slices this file touches
|
|
258
|
+
active_slices = set()
|
|
259
|
+
for composable, slice_id in COMPOSABLE_TO_SLICE.items():
|
|
260
|
+
if composable in file_content:
|
|
261
|
+
active_slices.add(slice_id)
|
|
262
|
+
|
|
263
|
+
# Return rules from active slices only
|
|
264
|
+
relevant = []
|
|
265
|
+
for sid in active_slices:
|
|
266
|
+
relevant.extend(by_slice.get(sid, []))
|
|
267
|
+
|
|
268
|
+
return relevant
|