@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,581 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""apply.py — Executable structural gate.
|
|
3
|
+
|
|
4
|
+
This is a STRUCTURAL GATE, not a correctness/compile verifier. It exists to
|
|
5
|
+
stop the AI from declaring a slice "done" and ending the turn before a
|
|
6
|
+
deterministic check has run — a forcing function on the state machine, paired
|
|
7
|
+
with the Stop hook (``guardrails/stop_require_apply_evidence.py``). The check
|
|
8
|
+
itself is intentionally minimal; it does NOT verify types, compilation, or
|
|
9
|
+
runtime behavior. Correctness comes from the slice's MUST/MUST NOT constraints
|
|
10
|
+
at generation time and the user running the code in their real project.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
python3 apply.py --slice conference/login-auth
|
|
14
|
+
python3 apply.py --unit foundation
|
|
15
|
+
[--session PATH]
|
|
16
|
+
[--project PATH]
|
|
17
|
+
|
|
18
|
+
Session path resolution (when --session not given):
|
|
19
|
+
1. $TRTC_SESSION_PATH env var
|
|
20
|
+
2. $CLAUDE_PROJECT_DIR/.trtc-session.yaml (Claude Code sets this to the
|
|
21
|
+
user project root)
|
|
22
|
+
3. ./.trtc-session.yaml (cwd fallback)
|
|
23
|
+
|
|
24
|
+
What it does:
|
|
25
|
+
|
|
26
|
+
1. Confirms the state machine is in ``code_written`` for the slice the
|
|
27
|
+
caller named. Anything else is a usage error (exit 2).
|
|
28
|
+
2. Requires the project to contain real source (``code non-empty``): if
|
|
29
|
+
``<project_root>/src/`` has no ``.vue``/``.ts`` files the run is
|
|
30
|
+
static-only and fails — there is nothing to gate.
|
|
31
|
+
3. For each slice, checks that the slice's **entry symbol** (its composable /
|
|
32
|
+
component, e.g. ``useDeviceState`` for device-control) appears as a real
|
|
33
|
+
code identifier in some source file — **with comments and string literals
|
|
34
|
+
stripped first** so the entry can't be faked from a ``// comment`` or
|
|
35
|
+
``"string"``. Slices with no registered entry symbol are skipped (we can't
|
|
36
|
+
check them mechanically; never a false-positive). This is a coarse
|
|
37
|
+
"did you wire up this capability's entry" check, deliberately NOT a proof
|
|
38
|
+
of correctness.
|
|
39
|
+
4. Runs a NARROW compile-safety check for composable-destructuring name
|
|
40
|
+
collisions (e.g. the same symbol destructured from two ``use*()`` calls,
|
|
41
|
+
or destructured and then re-declared as a function). It is NOT a general
|
|
42
|
+
linter.
|
|
43
|
+
5. Writes evidence JSON to ``<session_dir>/.trtc-apply-evidence/<slug>.json``.
|
|
44
|
+
6. Advances the state machine: pass → apply_passed (exit 0); fail →
|
|
45
|
+
apply_failed (exit 1).
|
|
46
|
+
|
|
47
|
+
Exit codes: 0 pass / 1 fail / 2 usage error
|
|
48
|
+
|
|
49
|
+
Why an entry-symbol check and not a per-API grep: an earlier version greped
|
|
50
|
+
each MUST rule's backtick-quoted patterns. That was unreliable in both
|
|
51
|
+
directions — any-pattern-hit produced false positives, and stripping string
|
|
52
|
+
literals (needed for anti-cheat) produced false negatives whenever a symbol
|
|
53
|
+
legitimately lived inside a string (e.g. an error-code constant). The gate's
|
|
54
|
+
real value is the forcing function (you cannot self-certify and stop), not the
|
|
55
|
+
judge's precision, so the judge was reduced to the honest minimum: code exists
|
|
56
|
+
+ the slice's entry was wired up. Correctness is delegated to the slice's
|
|
57
|
+
MUST/MUST NOT constraints and the customer's own build.
|
|
58
|
+
|
|
59
|
+
Why not a compiler: apply runs inside the customer's heterogeneous existing
|
|
60
|
+
project, where a build can fail for reasons unrelated to generated code
|
|
61
|
+
(missing deps, private registries, monorepo config) and is slow per slice —
|
|
62
|
+
so compilation is intentionally out of scope. The comment/string stripping is
|
|
63
|
+
kept because the demo-test-2 bug showed symbols can be stuffed into comments
|
|
64
|
+
or strings; entry symbols are code identifiers so this never false-negatives
|
|
65
|
+
on correct code.
|
|
66
|
+
"""
|
|
67
|
+
from __future__ import annotations
|
|
68
|
+
|
|
69
|
+
import argparse
|
|
70
|
+
import json
|
|
71
|
+
import os
|
|
72
|
+
import re
|
|
73
|
+
import sys
|
|
74
|
+
from pathlib import Path
|
|
75
|
+
|
|
76
|
+
import yaml
|
|
77
|
+
|
|
78
|
+
HERE = Path(__file__).resolve().parent
|
|
79
|
+
LIB_DIR = HERE / "lib"
|
|
80
|
+
|
|
81
|
+
sys.path.insert(0, str(LIB_DIR))
|
|
82
|
+
import state_machine # noqa: E402
|
|
83
|
+
sys.path.pop(0)
|
|
84
|
+
|
|
85
|
+
# rule_parser lives under trtc-apply/guardrails/apply_lib (sibling skill).
|
|
86
|
+
# HERE = skills/trtc-topic/scripts → parent.parent = skills/
|
|
87
|
+
_APPLY_GUARDRAILS = HERE.parent.parent / "trtc-apply" / "guardrails"
|
|
88
|
+
sys.path.insert(0, str(_APPLY_GUARDRAILS))
|
|
89
|
+
from apply_lib.rule_parser import entry_symbols_for_slice # noqa: E402
|
|
90
|
+
sys.path.pop(0)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _resolve_session_path() -> Path:
|
|
94
|
+
"""Match the resolver used by guardrails/gate_*.py:
|
|
95
|
+
env var → $CLAUDE_PROJECT_DIR → cwd. The session file lives in the
|
|
96
|
+
user project, never in the skill repo.
|
|
97
|
+
"""
|
|
98
|
+
explicit = os.environ.get("TRTC_SESSION_PATH")
|
|
99
|
+
if explicit:
|
|
100
|
+
return Path(explicit)
|
|
101
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR")
|
|
102
|
+
if project_dir:
|
|
103
|
+
return Path(project_dir) / ".trtc-session.yaml"
|
|
104
|
+
return Path.cwd() / ".trtc-session.yaml"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _eprint(msg: str) -> None:
|
|
108
|
+
sys.stderr.write(msg.rstrip("\n") + "\n")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _slice_id_to_slug(slice_id: str) -> str:
|
|
112
|
+
return slice_id.replace("/", "__")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _load_session(session_path: Path) -> dict:
|
|
116
|
+
return yaml.safe_load(session_path.read_text()) or {}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _strip_comments_and_strings(content: str) -> str:
|
|
120
|
+
"""Replace JS/TS comments and string literals with whitespace.
|
|
121
|
+
|
|
122
|
+
Why: the demo-test-2 root cause was AI stuffing the literal pattern
|
|
123
|
+
(e.g. ``Math.floor(Date.now() / 1000)``) into a ``//`` comment to pass
|
|
124
|
+
substring grep. After stripping, only real code is searched. Whitespace
|
|
125
|
+
is used (not deletion) so byte offsets are preserved if anyone ever
|
|
126
|
+
needs to surface code locations from the stripped buffer.
|
|
127
|
+
|
|
128
|
+
Strips:
|
|
129
|
+
* ``// single-line``
|
|
130
|
+
* ``/* block */`` (multi-line)
|
|
131
|
+
* ``"double-quoted"`` strings
|
|
132
|
+
* ``'single-quoted'`` strings
|
|
133
|
+
* `` `template literals` ``
|
|
134
|
+
"""
|
|
135
|
+
out: list[str] = []
|
|
136
|
+
i = 0
|
|
137
|
+
n = len(content)
|
|
138
|
+
while i < n:
|
|
139
|
+
# // single-line comment — skip to next newline
|
|
140
|
+
if content[i:i + 2] == "//":
|
|
141
|
+
end = content.find("\n", i)
|
|
142
|
+
if end == -1:
|
|
143
|
+
end = n
|
|
144
|
+
out.append(" " * (end - i))
|
|
145
|
+
i = end
|
|
146
|
+
# /* block comment */ — skip to */
|
|
147
|
+
elif content[i:i + 2] == "/*":
|
|
148
|
+
end = content.find("*/", i + 2)
|
|
149
|
+
if end == -1:
|
|
150
|
+
end = n
|
|
151
|
+
else:
|
|
152
|
+
end += 2
|
|
153
|
+
replaced = content[i:end]
|
|
154
|
+
# Preserve newlines inside the block; replace other chars with space.
|
|
155
|
+
out.append(re.sub(r"[^\n]", " ", replaced))
|
|
156
|
+
i = end
|
|
157
|
+
# double-quoted string
|
|
158
|
+
elif content[i] == '"':
|
|
159
|
+
j = i + 1
|
|
160
|
+
while j < n:
|
|
161
|
+
if content[j] == "\\" and j + 1 < n:
|
|
162
|
+
j += 2
|
|
163
|
+
elif content[j] == '"':
|
|
164
|
+
j += 1
|
|
165
|
+
break
|
|
166
|
+
else:
|
|
167
|
+
j += 1
|
|
168
|
+
out.append('""' + " " * max(0, j - i - 2))
|
|
169
|
+
i = j
|
|
170
|
+
# single-quoted string
|
|
171
|
+
elif content[i] == "'":
|
|
172
|
+
j = i + 1
|
|
173
|
+
while j < n:
|
|
174
|
+
if content[j] == "\\" and j + 1 < n:
|
|
175
|
+
j += 2
|
|
176
|
+
elif content[j] == "'":
|
|
177
|
+
j += 1
|
|
178
|
+
break
|
|
179
|
+
else:
|
|
180
|
+
j += 1
|
|
181
|
+
out.append("''" + " " * max(0, j - i - 2))
|
|
182
|
+
i = j
|
|
183
|
+
# template literal — note: doesn't fully parse ${...} interpolation,
|
|
184
|
+
# treats whole thing as a string. That's fine for our pattern matching.
|
|
185
|
+
elif content[i] == "`":
|
|
186
|
+
j = i + 1
|
|
187
|
+
while j < n:
|
|
188
|
+
if content[j] == "\\" and j + 1 < n:
|
|
189
|
+
j += 2
|
|
190
|
+
elif content[j] == "`":
|
|
191
|
+
j += 1
|
|
192
|
+
break
|
|
193
|
+
else:
|
|
194
|
+
j += 1
|
|
195
|
+
out.append("``" + " " * max(0, j - i - 2))
|
|
196
|
+
i = j
|
|
197
|
+
else:
|
|
198
|
+
out.append(content[i])
|
|
199
|
+
i += 1
|
|
200
|
+
return "".join(out)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _scan_project_src(project_root: Path) -> tuple[str, list[tuple[Path, str]]]:
|
|
204
|
+
"""Return (mode, [(path, stripped_content), ...]).
|
|
205
|
+
|
|
206
|
+
Each file's content is stripped of comments and string literals before
|
|
207
|
+
being returned, so the substring matcher only ever sees real code.
|
|
208
|
+
|
|
209
|
+
mode is either 'full' or 'static-only'. static-only means src/ doesn't
|
|
210
|
+
exist or has no source files.
|
|
211
|
+
"""
|
|
212
|
+
src = project_root / "src"
|
|
213
|
+
if not src.exists():
|
|
214
|
+
return "static-only", []
|
|
215
|
+
files: list[tuple[Path, str]] = []
|
|
216
|
+
for ext in ("*.vue", "*.ts"):
|
|
217
|
+
for f in src.rglob(ext):
|
|
218
|
+
try:
|
|
219
|
+
raw = f.read_text(encoding="utf-8")
|
|
220
|
+
except (OSError, UnicodeDecodeError):
|
|
221
|
+
continue
|
|
222
|
+
files.append((f, _strip_comments_and_strings(raw)))
|
|
223
|
+
return ("full" if files else "static-only", files)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
_IDENT = r"[A-Za-z_$][\w$]*"
|
|
227
|
+
# `const|let|var X =` / `function X` / `async function X` / `class X`
|
|
228
|
+
_SIMPLE_DECL_RE = re.compile(
|
|
229
|
+
r"\b(?:const|let|var)\s+(" + _IDENT + r")\s*[=:]"
|
|
230
|
+
r"|\b(?:async\s+)?function\s*\*?\s*(" + _IDENT + r")"
|
|
231
|
+
r"|\bclass\s+(" + _IDENT + r")"
|
|
232
|
+
)
|
|
233
|
+
# `const { a, b: c } = useXxx(...)` — destructuring whose RHS is a call.
|
|
234
|
+
_DESTRUCTURE_RE = re.compile(
|
|
235
|
+
r"\b(?:const|let|var)\s*\{([^{}]*)\}\s*=\s*([^\n;]*)"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _destructured_binding_names(brace_body: str) -> list[str]:
|
|
240
|
+
"""Extract the locally-bound identifiers from a `{ ... }` destructure body.
|
|
241
|
+
|
|
242
|
+
Handles ``a``, ``a: b`` (binds ``b``), ``a = default``, ``...rest``.
|
|
243
|
+
Skips nested destructuring parts (containing ``{``) — rare for composable
|
|
244
|
+
destructuring and not worth mis-parsing.
|
|
245
|
+
"""
|
|
246
|
+
names: list[str] = []
|
|
247
|
+
for part in brace_body.split(","):
|
|
248
|
+
part = part.strip()
|
|
249
|
+
if not part or "{" in part:
|
|
250
|
+
continue
|
|
251
|
+
if part.startswith("..."):
|
|
252
|
+
part = part[3:].strip()
|
|
253
|
+
if ":" in part: # rename: key:binding
|
|
254
|
+
part = part.split(":", 1)[1]
|
|
255
|
+
part = part.split("=", 1)[0].strip() # drop default value
|
|
256
|
+
if re.fullmatch(_IDENT, part):
|
|
257
|
+
names.append(part)
|
|
258
|
+
return names
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _check_duplicate_declarations(
|
|
262
|
+
files: list[tuple[Path, str]], project_root: Path
|
|
263
|
+
) -> list[dict]:
|
|
264
|
+
"""Detect composable-destructuring name collisions that will not compile.
|
|
265
|
+
|
|
266
|
+
This is a deliberately NARROW, high-precision check — NOT a general
|
|
267
|
+
redeclaration linter. It only flags the failure mode that apply's own
|
|
268
|
+
MUST-symbol grep can induce: the AI adds a destructured symbol (or a
|
|
269
|
+
wrapper function) to satisfy the grep, colliding with a name that was
|
|
270
|
+
already destructured from another composable. Two real demo bugs:
|
|
271
|
+
|
|
272
|
+
* ``const { getCameraList } = useDeviceState()`` + a later
|
|
273
|
+
``function getCameraList()`` in the same file.
|
|
274
|
+
* ``const { subscribeEvent } = useRoomParticipantState()`` +
|
|
275
|
+
``const { subscribeEvent } = useRoomState()`` in the same file.
|
|
276
|
+
|
|
277
|
+
We flag a name only when it is bound by destructuring at least twice, OR
|
|
278
|
+
bound by destructuring AND also declared as a simple const/function/class.
|
|
279
|
+
Pure simple-vs-simple duplicates are NOT flagged (those may be legal
|
|
280
|
+
locals in different scopes — we can't tell without a real parser).
|
|
281
|
+
"""
|
|
282
|
+
issues: list[dict] = []
|
|
283
|
+
for path, content in files:
|
|
284
|
+
destructured: list[str] = []
|
|
285
|
+
for m in _DESTRUCTURE_RE.finditer(content):
|
|
286
|
+
brace_body, rhs = m.group(1), m.group(2)
|
|
287
|
+
if "(" not in rhs: # only RHS that is a call (composable / hook)
|
|
288
|
+
continue
|
|
289
|
+
destructured.extend(_destructured_binding_names(brace_body))
|
|
290
|
+
simple: list[str] = []
|
|
291
|
+
for m in _SIMPLE_DECL_RE.finditer(content):
|
|
292
|
+
name = m.group(1) or m.group(2) or m.group(3)
|
|
293
|
+
if name:
|
|
294
|
+
simple.append(name)
|
|
295
|
+
try:
|
|
296
|
+
rel = str(path.resolve().relative_to(project_root.resolve()))
|
|
297
|
+
except (ValueError, OSError):
|
|
298
|
+
rel = str(path)
|
|
299
|
+
for name in sorted(set(destructured)):
|
|
300
|
+
d = destructured.count(name)
|
|
301
|
+
s = simple.count(name)
|
|
302
|
+
if d >= 2 or (d >= 1 and s >= 1):
|
|
303
|
+
issues.append(
|
|
304
|
+
{
|
|
305
|
+
"category": "duplicate-declaration",
|
|
306
|
+
"type": "critical",
|
|
307
|
+
"symbol": name,
|
|
308
|
+
"file": rel,
|
|
309
|
+
"rule_text": (
|
|
310
|
+
f"Duplicate declaration of '{name}' in {rel}: it is "
|
|
311
|
+
f"destructured from a composable and re-declared "
|
|
312
|
+
f"({d}x destructure, {s}x const/function) in the same "
|
|
313
|
+
f"file. This will not compile — alias one of them."
|
|
314
|
+
),
|
|
315
|
+
"slice_id": None,
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
return issues
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _check_slice_entry(
|
|
322
|
+
slice_id: str, files: list[tuple[Path, str]]
|
|
323
|
+
) -> tuple[str, list[str]]:
|
|
324
|
+
"""Check that a slice's entry symbol appears as real code.
|
|
325
|
+
|
|
326
|
+
Returns ``(result, entry_symbols)`` where ``result`` is one of:
|
|
327
|
+
|
|
328
|
+
* ``"pass"`` — at least one entry symbol appears as an identifier in
|
|
329
|
+
some file's real-code text (comments/strings stripped).
|
|
330
|
+
* ``"fail"`` — entry symbols are known but none appear.
|
|
331
|
+
* ``"skipped"`` — the slice has no registered entry symbol; it cannot be
|
|
332
|
+
checked mechanically and is treated as pass (we never
|
|
333
|
+
false-positive on slices we don't know how to check).
|
|
334
|
+
|
|
335
|
+
Entry symbols are stable code identifiers (composables / components), never
|
|
336
|
+
string literals, so unlike the old MUST-symbol grep this is immune to the
|
|
337
|
+
comment/string-stripping false-negative — yet the stripping is still run so
|
|
338
|
+
an entry mentioned only in a ``// comment`` or ``"string"`` does not count.
|
|
339
|
+
"""
|
|
340
|
+
entry_symbols = entry_symbols_for_slice(slice_id)
|
|
341
|
+
if not entry_symbols:
|
|
342
|
+
return "skipped", entry_symbols
|
|
343
|
+
for sym in entry_symbols:
|
|
344
|
+
word_re = re.compile(r"\b" + re.escape(sym) + r"\b")
|
|
345
|
+
for _, content in files:
|
|
346
|
+
if word_re.search(content):
|
|
347
|
+
return "pass", entry_symbols
|
|
348
|
+
return "fail", entry_symbols
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _write_evidence(
|
|
352
|
+
session_path: Path,
|
|
353
|
+
evidence_id: str,
|
|
354
|
+
payload: dict,
|
|
355
|
+
) -> Path:
|
|
356
|
+
ev_dir = session_path.parent / ".trtc-apply-evidence"
|
|
357
|
+
ev_dir.mkdir(parents=True, exist_ok=True)
|
|
358
|
+
out = ev_dir / f"{_slice_id_to_slug(evidence_id)}.json"
|
|
359
|
+
out.write_text(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
360
|
+
return out
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def main(argv: list[str] | None = None) -> int:
|
|
364
|
+
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
365
|
+
p.add_argument("--slice", dest="slice_id", default=None)
|
|
366
|
+
p.add_argument(
|
|
367
|
+
"--unit",
|
|
368
|
+
dest="unit_id",
|
|
369
|
+
default=None,
|
|
370
|
+
help="current delivery unit id",
|
|
371
|
+
)
|
|
372
|
+
p.add_argument(
|
|
373
|
+
"--session",
|
|
374
|
+
type=Path,
|
|
375
|
+
default=None,
|
|
376
|
+
help="explicit path to .trtc-session.yaml (overrides env-based resolver)",
|
|
377
|
+
)
|
|
378
|
+
p.add_argument("--project", type=Path, default=None)
|
|
379
|
+
args = p.parse_args(argv)
|
|
380
|
+
|
|
381
|
+
if not args.slice_id and not args.unit_id:
|
|
382
|
+
_eprint("error: provide --slice <slice_id> or --unit <unit_id>.")
|
|
383
|
+
return 2
|
|
384
|
+
|
|
385
|
+
session_path = args.session if args.session is not None else _resolve_session_path()
|
|
386
|
+
|
|
387
|
+
if not session_path.exists():
|
|
388
|
+
_eprint(
|
|
389
|
+
f"error: session file not found: {session_path}\n"
|
|
390
|
+
f" hint: cd to the user project root, or set $CLAUDE_PROJECT_DIR / "
|
|
391
|
+
f"$TRTC_SESSION_PATH before running this script."
|
|
392
|
+
)
|
|
393
|
+
return 2
|
|
394
|
+
|
|
395
|
+
# State machine gate.
|
|
396
|
+
scope = state_machine.current_scope(session_path)
|
|
397
|
+
if not scope.get("initialised"):
|
|
398
|
+
_eprint("error: queue not initialised; run init_slice_queue.py first.")
|
|
399
|
+
return 2
|
|
400
|
+
idx = scope["index"]
|
|
401
|
+
current_id = scope["id"]
|
|
402
|
+
state = scope["state"]
|
|
403
|
+
slice_ids = scope["slice_ids"]
|
|
404
|
+
kind = scope["kind"]
|
|
405
|
+
if args.slice_id != current_id:
|
|
406
|
+
if kind == "unit":
|
|
407
|
+
if args.unit_id:
|
|
408
|
+
if args.unit_id != current_id:
|
|
409
|
+
_eprint(
|
|
410
|
+
f"error: --unit '{args.unit_id}' does not match current unit "
|
|
411
|
+
f"[{idx}] '{current_id}'."
|
|
412
|
+
)
|
|
413
|
+
return 2
|
|
414
|
+
elif args.slice_id not in slice_ids:
|
|
415
|
+
_eprint(
|
|
416
|
+
f"error: --slice '{args.slice_id}' is not part of current unit "
|
|
417
|
+
f"[{idx}] '{current_id}' ({', '.join(slice_ids)})."
|
|
418
|
+
)
|
|
419
|
+
return 2
|
|
420
|
+
else:
|
|
421
|
+
_eprint(
|
|
422
|
+
f"error: --slice '{args.slice_id}' does not match current slice "
|
|
423
|
+
f"[{idx}] '{current_id}'."
|
|
424
|
+
)
|
|
425
|
+
return 2
|
|
426
|
+
if state != "code_written":
|
|
427
|
+
_eprint(
|
|
428
|
+
f"error: state must be 'code_written' to run apply; current state is '{state}'.\n"
|
|
429
|
+
f"hint: write the slice's code first, then run "
|
|
430
|
+
f"`next_slice.py advance mark_code_written` before calling apply.py."
|
|
431
|
+
)
|
|
432
|
+
return 2
|
|
433
|
+
|
|
434
|
+
# Resolve project root.
|
|
435
|
+
session_data = _load_session(session_path)
|
|
436
|
+
if args.project is not None:
|
|
437
|
+
project_root = args.project
|
|
438
|
+
else:
|
|
439
|
+
pr = (session_data.get("project_state") or {}).get("project_root")
|
|
440
|
+
project_root = Path(pr) if pr else None
|
|
441
|
+
if project_root is None:
|
|
442
|
+
_eprint("error: project root not provided and not in session.")
|
|
443
|
+
return 2
|
|
444
|
+
|
|
445
|
+
mode, project_files = _scan_project_src(project_root)
|
|
446
|
+
|
|
447
|
+
issues: list[dict] = []
|
|
448
|
+
per_slice: dict[str, dict] = {}
|
|
449
|
+
entries_checked = 0
|
|
450
|
+
for slice_id in slice_ids:
|
|
451
|
+
result, entry_symbols = _check_slice_entry(slice_id, project_files)
|
|
452
|
+
rec = {
|
|
453
|
+
"slice_id": slice_id,
|
|
454
|
+
"entry_checked": result != "skipped",
|
|
455
|
+
"entry_result": result,
|
|
456
|
+
"issues": [],
|
|
457
|
+
}
|
|
458
|
+
per_slice[slice_id] = rec
|
|
459
|
+
if result == "skipped":
|
|
460
|
+
continue
|
|
461
|
+
entries_checked += 1
|
|
462
|
+
if result == "fail":
|
|
463
|
+
# Naming the entry composable is safe and helpful: it is the
|
|
464
|
+
# slice's documented import (see its "代码生成约束 → 额外导入"
|
|
465
|
+
# section), not a hidden API pattern an LLM could comment-stuff.
|
|
466
|
+
issue = {
|
|
467
|
+
"rule_text": (
|
|
468
|
+
f"slice '{slice_id}': entry not wired up — none of its "
|
|
469
|
+
f"entry symbols ({', '.join(entry_symbols)}) appear in the "
|
|
470
|
+
f"generated code. Import and use the slice's documented "
|
|
471
|
+
f"entry (see its '额外导入' section)."
|
|
472
|
+
)[:200],
|
|
473
|
+
"type": "entry",
|
|
474
|
+
"slice_id": slice_id,
|
|
475
|
+
}
|
|
476
|
+
issues.append(issue)
|
|
477
|
+
rec["issues"].append(issue)
|
|
478
|
+
|
|
479
|
+
# Narrow compile-safety check: composable-destructuring name collisions.
|
|
480
|
+
# These are real compile errors, so they fail the gate. It is NOT a
|
|
481
|
+
# general redeclaration linter.
|
|
482
|
+
dup_issues = _check_duplicate_declarations(project_files, project_root)
|
|
483
|
+
for dup in dup_issues:
|
|
484
|
+
dup["slice_id"] = current_id
|
|
485
|
+
issues.append(dup)
|
|
486
|
+
per_slice.setdefault(
|
|
487
|
+
current_id,
|
|
488
|
+
{
|
|
489
|
+
"slice_id": current_id,
|
|
490
|
+
"entry_checked": False,
|
|
491
|
+
"entry_result": "skipped",
|
|
492
|
+
"issues": [],
|
|
493
|
+
},
|
|
494
|
+
)
|
|
495
|
+
per_slice[current_id]["issues"].append(dup)
|
|
496
|
+
|
|
497
|
+
status = "fail" if issues or mode == "static-only" else "pass"
|
|
498
|
+
evidence_id = current_id if kind == "unit" else slice_ids[0]
|
|
499
|
+
payload = {
|
|
500
|
+
"id": evidence_id,
|
|
501
|
+
"kind": kind,
|
|
502
|
+
"slice_id": slice_ids[0] if len(slice_ids) == 1 else None,
|
|
503
|
+
"unit_id": current_id if kind == "unit" else None,
|
|
504
|
+
"slice_ids": slice_ids,
|
|
505
|
+
"status": status,
|
|
506
|
+
"mode": mode,
|
|
507
|
+
"entries_checked": entries_checked,
|
|
508
|
+
"issues": issues,
|
|
509
|
+
"slices_checked": list(per_slice.values()),
|
|
510
|
+
"project_root": str(project_root),
|
|
511
|
+
"files_scanned": len(project_files),
|
|
512
|
+
}
|
|
513
|
+
_write_evidence(session_path, evidence_id, payload)
|
|
514
|
+
|
|
515
|
+
transition = "mark_apply_passed" if status == "pass" else "mark_apply_failed"
|
|
516
|
+
try:
|
|
517
|
+
state_machine.advance(session_path, transition)
|
|
518
|
+
except RuntimeError as exc:
|
|
519
|
+
_eprint(f"error: failed to advance state machine: {exc}")
|
|
520
|
+
return 2
|
|
521
|
+
|
|
522
|
+
if status == "pass":
|
|
523
|
+
# auto-advance policy: pause_on_failure / pause_at_end skip the
|
|
524
|
+
# explicit "ask user 继续?" pause when apply itself confirms success.
|
|
525
|
+
# pause_each (default) preserves the original per-slice prompt.
|
|
526
|
+
# Unknown / unset values fall back to pause_each — fail closed.
|
|
527
|
+
policy = session_data.get("auto_advance_policy")
|
|
528
|
+
if policy in {"pause_on_failure", "pause_at_end"}:
|
|
529
|
+
try:
|
|
530
|
+
new_state = state_machine.advance(session_path, "mark_user_confirmed")
|
|
531
|
+
except RuntimeError as exc:
|
|
532
|
+
_eprint(f"error: failed to auto-advance state machine: {exc}")
|
|
533
|
+
return 2
|
|
534
|
+
print(
|
|
535
|
+
f"apply pass: {entries_checked} slice entr"
|
|
536
|
+
f"{'y' if entries_checked == 1 else 'ies'} wired up for "
|
|
537
|
+
f"{evidence_id} — auto-advanced ({policy}); next state: {new_state}"
|
|
538
|
+
)
|
|
539
|
+
# POST-LOOP CHECKLIST: when all slices are done, remind the AI
|
|
540
|
+
# to execute Step 4 and Step 4.5 from topic/SKILL.md.
|
|
541
|
+
if new_state == "all_done":
|
|
542
|
+
print("")
|
|
543
|
+
print("=" * 60)
|
|
544
|
+
print("ALL SLICES COMPLETE — POST-LOOP CHECKLIST (mandatory)")
|
|
545
|
+
print("=" * 60)
|
|
546
|
+
print("The slice loop is finished, but the topic flow is NOT done.")
|
|
547
|
+
print("You MUST now execute these steps from topic/SKILL.md:")
|
|
548
|
+
print("")
|
|
549
|
+
print(" □ Step 4: Present the verification checklist to the user")
|
|
550
|
+
print(" □ Step 4.5: Offer runtime verification & telemetry")
|
|
551
|
+
print(" (ask consent if telemetry.opted_in is null)")
|
|
552
|
+
print("")
|
|
553
|
+
print("Do NOT output a final summary and stop. Read topic/SKILL.md")
|
|
554
|
+
print("Step 4 and Step 4.5 sections and execute them now.")
|
|
555
|
+
print("=" * 60)
|
|
556
|
+
return 0
|
|
557
|
+
print(
|
|
558
|
+
f"apply pass: {entries_checked} slice entr"
|
|
559
|
+
f"{'y' if entries_checked == 1 else 'ies'} wired up for {evidence_id}"
|
|
560
|
+
)
|
|
561
|
+
return 0
|
|
562
|
+
|
|
563
|
+
entry_failed = [i for i in issues if i.get("category") != "duplicate-declaration"]
|
|
564
|
+
dup_failed = [i for i in issues if i.get("category") == "duplicate-declaration"]
|
|
565
|
+
parts = []
|
|
566
|
+
if entry_failed:
|
|
567
|
+
parts.append(f"{len(entry_failed)} slice entr"
|
|
568
|
+
f"{'y' if len(entry_failed) == 1 else 'ies'} not wired up")
|
|
569
|
+
if dup_failed:
|
|
570
|
+
parts.append(f"{len(dup_failed)} duplicate-declaration issue(s)")
|
|
571
|
+
if mode == "static-only" and not parts:
|
|
572
|
+
parts.append("no source files found under src/")
|
|
573
|
+
summary = ", ".join(parts) if parts else "gate failed"
|
|
574
|
+
print(f"apply fail: {summary} for {evidence_id} (mode={mode})")
|
|
575
|
+
for issue in issues[:5]:
|
|
576
|
+
print(f" - {issue.get('rule_text', '')[:160]}")
|
|
577
|
+
return 1
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
if __name__ == "__main__":
|
|
581
|
+
sys.exit(main())
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""finalize_session.py — Normalize a completed TRTC topic session.
|
|
3
|
+
|
|
4
|
+
Run this only after topic Step 4 / Step 4.5 are finished and the integration
|
|
5
|
+
is genuinely complete. `current_execution_state=all_done` only means the
|
|
6
|
+
execution loop ended; this script is the explicit final handoff.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _resolve_session_path() -> Path:
|
|
20
|
+
explicit = os.environ.get("TRTC_SESSION_PATH")
|
|
21
|
+
if explicit:
|
|
22
|
+
return Path(explicit)
|
|
23
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR")
|
|
24
|
+
if project_dir:
|
|
25
|
+
return Path(project_dir) / ".trtc-session.yaml"
|
|
26
|
+
return Path.cwd() / ".trtc-session.yaml"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _dedupe_preserve_order(values) -> list:
|
|
30
|
+
seen = set()
|
|
31
|
+
out = []
|
|
32
|
+
for value in values or []:
|
|
33
|
+
if value in seen:
|
|
34
|
+
continue
|
|
35
|
+
seen.add(value)
|
|
36
|
+
out.append(value)
|
|
37
|
+
return out
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _normalize(data: dict) -> dict:
|
|
41
|
+
queue = data.get("execution_queue") or []
|
|
42
|
+
if not queue and data.get("slice_queue"):
|
|
43
|
+
queue = [
|
|
44
|
+
{
|
|
45
|
+
"id": entry.get("id"),
|
|
46
|
+
"type": "slice",
|
|
47
|
+
"title": entry.get("id"),
|
|
48
|
+
"status": entry.get("status", "pending"),
|
|
49
|
+
"slices": [entry.get("id")],
|
|
50
|
+
}
|
|
51
|
+
for entry in data.get("slice_queue") or []
|
|
52
|
+
if entry.get("id")
|
|
53
|
+
]
|
|
54
|
+
queue_ids = [
|
|
55
|
+
slice_id
|
|
56
|
+
for entry in queue
|
|
57
|
+
for slice_id in (entry.get("slices") or [entry.get("id")])
|
|
58
|
+
if slice_id
|
|
59
|
+
]
|
|
60
|
+
completed = _dedupe_preserve_order(data.get("completed_steps") or [])
|
|
61
|
+
for slice_id in queue_ids:
|
|
62
|
+
if slice_id not in completed:
|
|
63
|
+
completed.append(slice_id)
|
|
64
|
+
|
|
65
|
+
for entry in queue:
|
|
66
|
+
if all(slice_id in completed for slice_id in (entry.get("slices") or [entry.get("id")])):
|
|
67
|
+
entry["status"] = "done"
|
|
68
|
+
|
|
69
|
+
data["status"] = "completed"
|
|
70
|
+
data["current_step"] = "completed"
|
|
71
|
+
data["completed_steps"] = completed
|
|
72
|
+
|
|
73
|
+
if queue:
|
|
74
|
+
data["execution_queue"] = queue
|
|
75
|
+
data["current_execution_index"] = len(queue)
|
|
76
|
+
data["current_execution_state"] = "all_done"
|
|
77
|
+
|
|
78
|
+
data["updated_at"] = datetime.now(timezone.utc).astimezone().isoformat()
|
|
79
|
+
|
|
80
|
+
product = data.get("product") or "TRTC"
|
|
81
|
+
scenario = data.get("scenario") or data.get("intent") or "integration"
|
|
82
|
+
done_count = len(completed)
|
|
83
|
+
data["last_recap"] = (
|
|
84
|
+
f"{product} {scenario} integration completed. "
|
|
85
|
+
f"{done_count} step{'s' if done_count != 1 else ''} completed."
|
|
86
|
+
)
|
|
87
|
+
return data
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main(argv: list[str] | None = None) -> int:
|
|
91
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"--session",
|
|
94
|
+
type=Path,
|
|
95
|
+
default=None,
|
|
96
|
+
help="explicit path to .trtc-session.yaml (overrides env-based resolver)",
|
|
97
|
+
)
|
|
98
|
+
args = parser.parse_args(argv)
|
|
99
|
+
|
|
100
|
+
session_path = args.session if args.session is not None else _resolve_session_path()
|
|
101
|
+
if not session_path.exists():
|
|
102
|
+
print(f"error: session file not found at {session_path}", file=sys.stderr)
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
data = yaml.safe_load(session_path.read_text()) or {}
|
|
106
|
+
data = _normalize(data)
|
|
107
|
+
session_path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True))
|
|
108
|
+
print(f"session finalized — status=completed, current_step=completed")
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
sys.exit(main())
|