@event4u/agent-config 14.12.0 → 14.13.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +234 -0
- package/CONTRIBUTING.md +38 -0
- package/CREDITS.md +135 -0
- package/NOTICE +13 -0
- package/README.md +8 -6
- package/dist/agent-src/commands/analyze/inbox.md +109 -2
- package/dist/agent-src/commands/feature/dev.md +7 -2
- package/dist/agent-src/commands/ghostwriter/write.md +5 -1
- package/dist/agent-src/commands/implement-ticket.md +1 -1
- package/dist/agent-src/commands/mission/upgrade.md +1 -1
- package/dist/agent-src/commands/orchestrate.md +2 -2
- package/dist/agent-src/commands/post-as/me.md +5 -1
- package/dist/agent-src/commands/pr/merge.md +19 -6
- package/dist/agent-src/commands/roadmap/process-full.md +19 -5
- package/dist/agent-src/commands/tdd/red.md +12 -2
- package/dist/agent-src/commands/work.md +1 -1
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +40 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -2
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +21 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +1 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +40 -0
- package/dist/agent-src/contexts/execution/subagent-steering.md +13 -1
- package/dist/agent-src/contexts/execution/terminal-states.md +96 -0
- package/dist/agent-src/rules/artifact-drafting-protocol.md +1 -1
- package/dist/agent-src/rules/broken-access-control.md +4 -0
- package/dist/agent-src/rules/code-provenance.md +4 -0
- package/dist/agent-src/rules/design-fidelity.md +1 -1
- package/dist/agent-src/rules/domain-safety-pii.md +4 -0
- package/dist/agent-src/rules/domain-safety-retention.md +4 -0
- package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +14 -12
- package/dist/agent-src/rules/improve-before-implement.md +6 -8
- package/dist/agent-src/rules/laravel-routing.md +1 -1
- package/dist/agent-src/rules/lethal-trifecta-guard.md +4 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +2 -0
- package/dist/agent-src/rules/source-confidentiality.md +12 -34
- package/dist/agent-src/rules/symfony-routing.md +1 -1
- package/dist/agent-src/rules/token-efficiency.md +14 -0
- package/dist/agent-src/rules/tool-safety.md +4 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +4 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +247 -3
- package/dist/agent-src/scripts/guarded_baseline.ts +328 -0
- package/dist/agent-src/scripts/roadmap_set_step.ts +253 -0
- package/dist/agent-src/scripts/stub_queue.ts +82 -0
- package/dist/agent-src/scripts/stubs_due.ts +400 -0
- package/dist/agent-src/scripts/ticket_bundles.ts +84 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +8 -4
- package/dist/agent-src/skills/brand-asset-generation/SKILL.md +17 -4
- package/dist/agent-src/skills/brand-audit/SKILL.md +33 -0
- package/dist/agent-src/skills/code-intelligence/SKILL.md +57 -0
- package/dist/agent-src/skills/database/SKILL.md +8 -0
- package/dist/agent-src/skills/dependency-upgrade/SKILL.md +27 -0
- package/dist/agent-src/skills/design-review/SKILL.md +1 -1
- package/dist/agent-src/skills/doc-coauthoring/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +17 -1
- package/dist/agent-src/skills/feature-planning/SKILL.md +9 -2
- package/dist/agent-src/skills/frontend-render-security/SKILL.md +44 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +1 -1
- package/dist/agent-src/skills/js-library-packaging/scripts/check_package_surface.ts +38 -4
- package/dist/agent-src/skills/judge-artifact-completeness/rubrics/ticket-quality-score.json +2 -2
- package/dist/agent-src/skills/laravel/SKILL.md +35 -0
- package/dist/agent-src/skills/laravel-migration/SKILL.md +57 -3
- package/dist/agent-src/skills/monorepo-workspace/SKILL.md +26 -0
- package/dist/agent-src/skills/playbook-authoring/SKILL.md +28 -1
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/references/procedure.md +5 -5
- package/dist/agent-src/skills/sql-writing/SKILL.md +7 -1
- package/dist/agent-src/skills/standards-from-config/SKILL.md +27 -4
- package/dist/agent-src/skills/supply-chain-intake/SKILL.md +11 -4
- package/dist/agent-src/skills/symfony-workflow/SKILL.md +35 -0
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +85 -0
- package/dist/agent-src/skills/test-driven-development/SKILL.md +33 -9
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +1 -1
- package/dist/agent-src/skills/testing-anti-patterns/process-anti-patterns.md +37 -0
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +45 -2
- package/dist/agent-src/skills/upstream-contribute/SKILL.md +38 -1
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +9 -4
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +97 -3
- package/dist/agent-src/templates/scripts/check_memory.ts +258 -17
- package/dist/agent-src/templates/scripts/check_memory_proposal.ts +9 -3
- package/dist/agent-src/templates/scripts/memory_lookup.ts +86 -0
- package/dist/agent-src/templates/scripts/memory_signal.ts +44 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +90 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/index.ts +11 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +23 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +10 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +5 -1
- package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +102 -14
- package/dist/cli/commands/uiAudit.js +124 -5
- package/dist/cli/registry.js +2 -0
- package/dist/cli-delegate/{chunk-HYWSRNUX.js → chunk-2DK2VJWG.js} +3 -3
- package/dist/cli-delegate/{chunk-JCW7ZLHK.js → chunk-4FS4WRXP.js} +5 -0
- package/dist/cli-delegate/{chunk-OMIVVCG2.js → chunk-A4QJ6ENT.js} +24 -0
- package/dist/cli-delegate/{chunk-JJA2QEWA.js → chunk-AKJGUQNO.js} +466 -285
- package/dist/cli-delegate/{chunk-E46KPN6K.js → chunk-BTWUYDGN.js} +424 -363
- package/dist/cli-delegate/{chunk-K4FFODTS.js → chunk-ILMUPLGW.js} +1227 -316
- package/dist/cli-delegate/{chunk-FF7WAFMR.js → chunk-KPBWFOPZ.js} +5 -1
- package/dist/cli-delegate/cmd_adr_effective.js +102 -0
- package/dist/cli-delegate/cmd_conformance.js +42 -10
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_export.js +3 -2
- package/dist/cli-delegate/cmd_fleet.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_preflight.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +3 -2
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +3 -2
- package/dist/cli-delegate/cmd_settings_set.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +3 -2
- package/dist/cli-delegate/cmd_uninstall.js +3 -2
- package/dist/cli-delegate/cmd_validate.js +3 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +57 -57
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1836 -712
- package/dist/install/claudeRuleRewrite.js +254 -0
- package/dist/install/detect.js +29 -16
- package/dist/install/detect_php_shape.js +159 -0
- package/dist/install/install.mjs +634 -428
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +3 -3
- package/dist/mcp/server.mjs +4818 -2645
- package/dist/server/schemas/settings.js +3 -0
- package/dist/ui/assets/index-D0jaCRPM.js +29 -0
- package/dist/ui/index.html +1 -1
- package/docs/MIGRATION.md +1 -1
- package/docs/THIRD-PARTY-NOTICES.md +25 -0
- package/docs/contracts/provider-lifecycle.md +1 -1
- package/docs/contracts/settings-classes.md +10 -4
- package/docs/guidelines/agent-infra/5w2h-analysis.md +3 -3
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -0
- package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +58 -3
- package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +2 -2
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +3 -3
- package/docs/guidelines/agent-infra/break-glass-usage.md +7 -7
- package/docs/guidelines/agent-infra/carve-out-predicates.md +3 -3
- package/docs/guidelines/agent-infra/comparison-matrix.md +1 -1
- package/docs/guidelines/agent-infra/critical-thinking.md +4 -4
- package/docs/guidelines/agent-infra/direct-answers-demos.md +2 -2
- package/docs/guidelines/agent-infra/domain-eval-anti-pattern.md +1 -1
- package/docs/guidelines/agent-infra/engineering-memory-data-format.md +3 -3
- package/docs/guidelines/agent-infra/existence-question-verdicts.md +80 -0
- package/docs/guidelines/agent-infra/first-principles.md +2 -2
- package/docs/guidelines/agent-infra/framework-neutrality-patterns.md +2 -2
- package/docs/guidelines/agent-infra/guarded-baseline.md +39 -0
- package/docs/guidelines/agent-infra/inversion-thinking.md +5 -5
- package/docs/guidelines/agent-infra/language-and-tone-examples.md +1 -1
- package/docs/guidelines/agent-infra/layered-settings.md +10 -10
- package/docs/guidelines/agent-infra/memory-access.md +1 -1
- package/docs/guidelines/agent-infra/mental-models.md +3 -3
- package/docs/guidelines/agent-infra/reuse-verdict-mechanics.md +71 -0
- package/docs/guidelines/agent-infra/review-routing-data-format.md +7 -7
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +13 -7
- package/docs/guidelines/agent-infra/role-contracts.md +2 -2
- package/docs/guidelines/agent-infra/role-mode-router.md +3 -3
- package/docs/guidelines/agent-infra/rule-type-governance.md +2 -2
- package/docs/guidelines/agent-infra/scqa-framework.md +5 -5
- package/docs/guidelines/agent-infra/self-improvement-pipeline.md +6 -6
- package/docs/guidelines/agent-infra/simplicity-and-goal-demos.md +2 -2
- package/docs/guidelines/agent-infra/six-hats.md +3 -3
- package/docs/guidelines/agent-infra/skill-quality-checklist.md +6 -6
- package/docs/guidelines/agent-infra/source-confidentiality-mechanics.md +109 -0
- package/docs/guidelines/agent-infra/systems-thinking.md +1 -1
- package/docs/guidelines/agent-infra/verify-before-complete-demos.md +2 -2
- package/docs/guidelines/augment-portability-patterns.md +6 -4
- package/docs/guidelines/component-oriented-and-oop-development.md +3 -2
- package/docs/guidelines/cross-role-handoff.md +2 -2
- package/docs/guidelines/design-fidelity-mechanics.md +1 -1
- package/docs/guidelines/docs/readme-size-and-splitting.md +3 -3
- package/docs/guidelines/php/database.md +90 -7
- package/docs/guidelines/prompt-templates.md +7 -7
- package/llms.txt +2 -2
- package/package.json +5 -2
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +247 -3
- package/src/agent-src/scripts/guarded_baseline.ts +328 -0
- package/src/agent-src/scripts/roadmap_set_step.ts +253 -0
- package/src/agent-src/scripts/stub_queue.ts +82 -0
- package/src/agent-src/scripts/stubs_due.ts +400 -0
- package/src/agent-src/scripts/ticket_bundles.ts +84 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +8 -4
- package/src/agent-src/templates/scripts/check_memory.ts +258 -17
- package/src/agent-src/templates/scripts/check_memory_proposal.ts +9 -3
- package/src/agent-src/templates/scripts/memory_lookup.ts +86 -0
- package/src/agent-src/templates/scripts/memory_signal.ts +44 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +90 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +11 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +23 -1
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +10 -1
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +5 -1
- package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +102 -14
- package/src/config/agent-settings.template.yml +32 -3
- package/src/config/assurance-capability-registry.json +21 -9
- package/src/config/canonical-terms.yml +119 -0
- package/src/config/capability-native-outcome-bars.json +81 -0
- package/src/config/ci-local-parity.yml +80 -0
- package/src/config/drafting-channels.yml +43 -0
- package/src/config/estate-count-budget.json +38 -36
- package/src/config/evaluator-budgets.json +14 -14
- package/src/config/gate-coverage.yml +579 -63
- package/src/config/gate-reachability-exemptions.json +21 -0
- package/src/config/gate-violation-baselines.json +37 -19
- package/src/config/harness-evolution-budget.json +25 -0
- package/src/config/lapsed-beta-baseline.json +96 -0
- package/src/config/memory-twin-verdicts.yml +129 -0
- package/src/config/metric-registry.yml +124 -0
- package/src/config/pack-size-budget.json +16 -15
- package/src/config/preamble-payload-budget.json +8 -8
- package/src/config/profiles/balanced.ini +27 -0
- package/src/config/profiles/full.ini +27 -0
- package/src/config/profiles/minimal.ini +27 -0
- package/src/config/publish-surface.json +9 -2
- package/src/config/routing-coverage-seed.json +16 -2
- package/src/config/rule-stub-ceilings.json +126 -112
- package/src/config/turnaround-budget.json +66 -0
- package/src/install/claudeRuleRewrite.ts +288 -0
- package/src/install/detect.ts +60 -17
- package/src/install/detect_php_shape.ts +191 -0
- package/src/scripts/_cli/cmd_doctor.ts +27 -112
- package/src/scripts/_dispatch.bash +27 -0
- package/src/scripts/_lib/activation_ladder.ts +211 -0
- package/src/scripts/_lib/activation_states.ts +88 -0
- package/src/scripts/_lib/adherence_detectors.ts +143 -0
- package/src/scripts/_lib/arith_claims.ts +181 -0
- package/src/scripts/_lib/arm_ranking.ts +62 -0
- package/src/scripts/_lib/asset_delivery_ledger.ts +303 -0
- package/src/scripts/_lib/audit_field_provenance.ts +54 -0
- package/src/scripts/_lib/auto_dispatch.ts +32 -3
- package/src/scripts/_lib/bench_ab_size_claim.ts +98 -4
- package/src/scripts/_lib/benchmark_cache_fields.ts +158 -0
- package/src/scripts/_lib/candidate_proposer.ts +402 -0
- package/src/scripts/_lib/candidate_record.ts +601 -0
- package/src/scripts/_lib/capture_rate.ts +319 -0
- package/src/scripts/_lib/collector_denominator.ts +771 -0
- package/src/scripts/_lib/collector_record.ts +340 -0
- package/src/scripts/_lib/collector_store.ts +664 -0
- package/src/scripts/_lib/collector_supervision.ts +827 -0
- package/src/scripts/_lib/concern_estate.ts +77 -0
- package/src/scripts/_lib/config_chain.ts +477 -0
- package/src/scripts/_lib/config_cost.ts +209 -0
- package/src/scripts/_lib/council_settings_block.ts +6 -0
- package/src/scripts/_lib/curator_ops.ts +172 -0
- package/src/scripts/_lib/delivery_arm_experiment.ts +266 -0
- package/src/scripts/_lib/delivery_criticality.ts +126 -0
- package/src/scripts/_lib/doctor_runtime_checks.ts +70 -0
- package/src/scripts/_lib/empty_cycles.ts +88 -0
- package/src/scripts/_lib/eval_publication.ts +354 -0
- package/src/scripts/_lib/evaluation_vector.ts +283 -0
- package/src/scripts/_lib/evaluator_promotion.ts +225 -0
- package/src/scripts/_lib/evidence_basis.ts +118 -0
- package/src/scripts/_lib/exec_evidence.ts +8 -0
- package/src/scripts/_lib/experience_card.ts +272 -0
- package/src/scripts/_lib/experience_report.ts +151 -0
- package/src/scripts/_lib/experiment_freeze.ts +112 -0
- package/src/scripts/_lib/generated_by.ts +66 -0
- package/src/scripts/_lib/harness_evolution_guards.ts +229 -0
- package/src/scripts/_lib/hook_effect_probe.ts +141 -0
- package/src/scripts/_lib/host_denominator.ts +687 -0
- package/src/scripts/_lib/host_projection_reach.ts +157 -0
- package/src/scripts/_lib/ignored_blocker.ts +148 -0
- package/src/scripts/_lib/install_reach_checks.ts +495 -0
- package/src/scripts/_lib/json_duplicate_keys.ts +150 -0
- package/src/scripts/_lib/judge_hygiene.ts +228 -0
- package/src/scripts/_lib/legacy_boundary_map.ts +228 -0
- package/src/scripts/_lib/loop_guards.ts +299 -0
- package/src/scripts/_lib/loss_class.ts +129 -0
- package/src/scripts/_lib/machine_wake.ts +51 -0
- package/src/scripts/_lib/md_prose_lines.ts +177 -0
- package/src/scripts/_lib/minimality_tiebreak.ts +168 -0
- package/src/scripts/_lib/one_resolver_invariant.ts +449 -0
- package/src/scripts/_lib/orchestration_record.ts +253 -5
- package/src/scripts/_lib/outcome_envelope.ts +227 -0
- package/src/scripts/_lib/outcome_vocabularies.ts +125 -0
- package/src/scripts/_lib/paired_stats.ts +245 -0
- package/src/scripts/_lib/paired_verdict.ts +226 -0
- package/src/scripts/_lib/prefix_stable_surfaces.ts +123 -0
- package/src/scripts/_lib/privacy_class.ts +47 -0
- package/src/scripts/_lib/regression_neighbourhood.ts +259 -0
- package/src/scripts/_lib/repeated_failure.ts +105 -0
- package/src/scripts/_lib/repo_root.ts +111 -0
- package/src/scripts/_lib/review_skipped_record.ts +37 -0
- package/src/scripts/_lib/role_split.ts +338 -0
- package/src/scripts/_lib/runtime_journal.ts +1499 -0
- package/src/scripts/_lib/runtime_wiring_checks.ts +430 -0
- package/src/scripts/_lib/skill_catalogue.ts +11 -1
- package/src/scripts/_lib/source_digest.ts +200 -0
- package/src/scripts/_lib/source_redact.ts +260 -0
- package/src/scripts/_lib/source_shape.ts +502 -0
- package/src/scripts/_lib/source_snapshot_dedup.ts +199 -0
- package/src/scripts/_lib/strip_comments.d.mts +8 -0
- package/src/scripts/_lib/strip_comments.mjs +65 -0
- package/src/scripts/_lib/test_red_state.ts +154 -0
- package/src/scripts/_lib/trigger_shift.ts +144 -0
- package/src/scripts/_lib/untrusted_content.ts +82 -7
- package/src/scripts/adoption_report.ts +1 -1
- package/src/scripts/adr/evidence_census.ts +1 -1
- package/src/scripts/adr/regenerate_index.ts +26 -2
- package/src/scripts/adr_cite_check.ts +164 -1
- package/src/scripts/ai_council/blind_review.ts +11 -1
- package/src/scripts/ai_council/clients.ts +4 -0
- package/src/scripts/ai_council/config.ts +3 -0
- package/src/scripts/ai_council/consensus.ts +28 -8
- package/src/scripts/ai_council/consensus_round.ts +226 -0
- package/src/scripts/ai_council/disagreement_signal.ts +338 -0
- package/src/scripts/ai_council/inline_findings.ts +340 -0
- package/src/scripts/ai_council/judge_position_bias.ts +197 -0
- package/src/scripts/ai_council/orchestrator.ts +16 -203
- package/src/scripts/ai_council/orchestrator_results.ts +23 -3
- package/src/scripts/ai_council/pricing.ts +106 -0
- package/src/scripts/ai_council/prompts.ts +74 -7
- package/src/scripts/ai_council/provider_leakage_bench.ts +327 -0
- package/src/scripts/ai_council/py_format.ts +14 -3
- package/src/scripts/ai_council/recouncil_guard.ts +293 -0
- package/src/scripts/ai_council/response_record.ts +50 -0
- package/src/scripts/ai_council/topology_bench_manifest.ts +860 -0
- package/src/scripts/ai_council/topology_vocabulary.ts +175 -0
- package/src/scripts/asset_delivery_ledger.ts +157 -0
- package/src/scripts/attest_artifact.ts +193 -0
- package/src/scripts/audit_auto_rules.ts +2 -1
- package/src/scripts/audit_command_surface.ts +1 -1
- package/src/scripts/audit_mcp_tools.ts +2 -2
- package/src/scripts/audit_skill_overlap_allowlist.json +6 -1
- package/src/scripts/audit_user_type_axis.ts +2 -1
- package/src/scripts/bench_ab_clone.ts +215 -10
- package/src/scripts/bench_ab_integrity.ts +119 -5
- package/src/scripts/bench_ab_v2_stats.ts +40 -213
- package/src/scripts/build_archive_index.ts +1 -1
- package/src/scripts/build_linear_digest.ts +1 -1
- package/src/scripts/build_proof.ts +11 -4
- package/src/scripts/build_rule_trigger_matrix.ts +1 -1
- package/src/scripts/build_source_digests.ts +157 -0
- package/src/scripts/cache_realization_report.ts +103 -0
- package/src/scripts/check_adr_frontmatter.ts +126 -0
- package/src/scripts/check_benchmark_report_fields.ts +207 -0
- package/src/scripts/check_beta_review_markers.ts +235 -6
- package/src/scripts/check_bundle_path_leakage.ts +34 -1
- package/src/scripts/check_claims.ts +576 -6
- package/src/scripts/check_composite_arming.ts +35 -0
- package/src/scripts/check_concern_admissions.ts +365 -0
- package/src/scripts/check_enforcement_coverage.ts +28 -0
- package/src/scripts/check_estate_count.ts +142 -1
- package/src/scripts/check_gate_reachability.ts +383 -0
- package/src/scripts/check_host_loadability.ts +104 -0
- package/src/scripts/check_loss_class_declared.ts +308 -0
- package/src/scripts/check_md_language.ts +15 -98
- package/src/scripts/check_memory_proposal.ts +18 -11
- package/src/scripts/check_no_currency_in_cost_surfaces.ts +225 -0
- package/src/scripts/check_no_external_sources.ts +281 -6
- package/src/scripts/check_pack_conformance_fixture.ts +11 -1
- package/src/scripts/check_pack_size.ts +59 -2
- package/src/scripts/check_preamble_payload_budget.ts +114 -3
- package/src/scripts/check_prefix_stable_mutation.ts +372 -0
- package/src/scripts/check_publish_surface.ts +10 -1
- package/src/scripts/check_references.ts +24 -1
- package/src/scripts/check_rule_evidence_declaration.ts +316 -0
- package/src/scripts/check_rule_projection_integrity.ts +14 -4
- package/src/scripts/check_score_contract.ts +26 -3
- package/src/scripts/check_static_parity.ts +285 -0
- package/src/scripts/check_supervision_claim_atomicity.ts +498 -0
- package/src/scripts/check_suppression_hygiene.ts +15 -0
- package/src/scripts/ci_settle.ts +39 -2
- package/src/scripts/code_graph/build.ts +60 -3
- package/src/scripts/code_graph/extract.ts +44 -0
- package/src/scripts/code_graph/types.ts +24 -0
- package/src/scripts/collector_daemon.ts +714 -0
- package/src/scripts/config_cost_report.ts +302 -0
- package/src/scripts/consumer_matrix.ts +74 -1
- package/src/scripts/cost/budget.mjs +10 -3
- package/src/scripts/cost/track.mjs +10 -3
- package/src/scripts/council_cli.ts +37 -54
- package/src/scripts/dispatch_r2_reviewer.ts +46 -5
- package/src/scripts/evolution_lab.ts +980 -0
- package/src/scripts/external_sources_denylist.json +5 -18
- package/src/scripts/extract_audit_patterns.ts +21 -0
- package/src/scripts/fold_intake.ts +12 -1
- package/src/scripts/gen_discovery_baseline.ts +1 -1
- package/src/scripts/generate_capabilities_index.ts +12 -5
- package/src/scripts/generate_capability_matrix.ts +3 -3
- package/src/scripts/generate_command_flows.ts +2 -2
- package/src/scripts/generate_cookbook.ts +2 -2
- package/src/scripts/generate_index.ts +8 -1
- package/src/scripts/generate_knowledge_index.ts +2 -1
- package/src/scripts/generate_ownership_matrix.ts +2 -1
- package/src/scripts/generate_pack_manifests.ts +10 -2
- package/src/scripts/generate_role_experiences_catalog.ts +2 -2
- package/src/scripts/generate_settings_reference.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +22 -1
- package/src/scripts/grade_target_readiness.ts +48 -5
- package/src/scripts/hook_effect_doctor.ts +261 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +98 -11
- package/src/scripts/hooks/block_speaking_inbox_dir.ts +271 -0
- package/src/scripts/hooks/block_unauthorized_git.ts +18 -0
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/dispatch_hook.ts +32 -0
- package/src/scripts/hooks/journal_record_hook.ts +345 -0
- package/src/scripts/hooks/prepush_metadata_sources.sh +110 -0
- package/src/scripts/hooks/run_continuation_hook.ts +44 -9
- package/src/scripts/hooks/shims/install.sh +61 -0
- package/src/scripts/hooks/shims/php +101 -0
- package/src/scripts/hooks/suggestion_capture_hook.ts +10 -0
- package/src/scripts/hot_context_hook.ts +16 -0
- package/src/scripts/install-hooks.sh +20 -5
- package/src/scripts/install.ts +17 -35
- package/src/scripts/inventory_frontmatter.ts +2 -1
- package/src/scripts/lint_adr_sweep_routing.ts +12 -0
- package/src/scripts/lint_canonical_terms.ts +604 -0
- package/src/scripts/lint_composition_review.ts +619 -0
- package/src/scripts/lint_consolidation_lineage.ts +666 -0
- package/src/scripts/lint_eval_specs.ts +593 -0
- package/src/scripts/lint_hook_manifest.ts +19 -0
- package/src/scripts/lint_host_portability.ts +373 -0
- package/src/scripts/lint_judge_prompt_expectation.ts +27 -1
- package/src/scripts/lint_memory_twin_parity.ts +335 -0
- package/src/scripts/lint_metric_consumers.ts +355 -0
- package/src/scripts/lint_skill_descriptions.ts +18 -1
- package/src/scripts/lint_skill_frontmatter_safety.ts +58 -0
- package/src/scripts/lint_skill_link_reach.ts +252 -0
- package/src/scripts/lint_skill_trigger_corpus.ts +426 -23
- package/src/scripts/lint_ticket_buildable.ts +17 -1
- package/src/scripts/mcp_server/consumer_tool_catalog.json +1 -1
- package/src/scripts/mcp_server/tool_catalog_source.ts +1 -1
- package/src/scripts/measure_docs_dead_links.ts +348 -0
- package/src/scripts/measure_host_capture.ts +461 -0
- package/src/scripts/migration_status.ts +3 -3
- package/src/scripts/model_rule_injection.ts +60 -0
- package/src/scripts/orchestration_record.ts +14 -0
- package/src/scripts/preamble_byte_census.ts +139 -10
- package/src/scripts/prepack-check.mjs +4 -1
- package/src/scripts/probe_turnaround.ts +384 -0
- package/src/scripts/probe_unblocked_ask.ts +279 -0
- package/src/scripts/render_benchmark_md.ts +1 -1
- package/src/scripts/rule_activation_census.ts +70 -9
- package/src/scripts/rule_backlinks.ts +1 -1
- package/src/scripts/run_lifecycle_suite.ts +208 -0
- package/src/scripts/run_skill_evals.ts +40 -0
- package/src/scripts/schemas/delivery-manifest.schema.json +43 -0
- package/src/scripts/schemas/drafting-channels.schema.json +43 -0
- package/src/scripts/schemas/evals.schema.json +24 -1
- package/src/scripts/schemas/rule.schema.json +52 -1
- package/src/scripts/schemas/skill.schema.json +17 -2
- package/src/scripts/skill_overlap.ts +7 -1
- package/src/scripts/skill_tools/run_block_d_eval.ts +8 -1
- package/src/scripts/skill_tools/score_skill_relevance.ts +84 -3
- package/src/scripts/skill_tools/suggest_skill_for_task.ts +8 -1
- package/src/scripts/skill_usage_report.ts +2 -1
- package/src/scripts/stubs_due.ts +211 -0
- package/src/scripts/sweep_source_surfaces.ts +678 -0
- package/src/scripts/sync_pr_branch.ts +3 -3
- package/src/scripts/trigger_eval_grandfather.json +202 -222
- package/src/scripts/utilization_report.ts +22 -2
- package/src/server/schemas/settings.ts +5 -0
- package/src/templates/marketing-copy.yml +1 -1
- package/dist/ui/assets/index-Cxgy63kz.js +0 -26
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Memory-twin parity — the copies may not drift apart again.
|
|
4
|
+
*
|
|
5
|
+
* `road-to-memory-twin-reconciliation` Phase 3. Seven scripts exist twice, once
|
|
6
|
+
* under `src/scripts/` and once under `src/agent-src/templates/scripts/`, and
|
|
7
|
+
* `package.json:files[]` ships BOTH. Until this gate there was no sync, no drift
|
|
8
|
+
* check and no parity check between them — so they diverged in both directions
|
|
9
|
+
* and the divergence was found by a redundancy audit rather than by CI.
|
|
10
|
+
*
|
|
11
|
+
* ── What it compares ────────────────────────────────────────────────────────
|
|
12
|
+
* NON-COMMENT lines only. Block comments, line comments and blank lines are
|
|
13
|
+
* stripped from both sides before diffing, because the docstring differences
|
|
14
|
+
* between the copies are known-correct: the template's header explains what a
|
|
15
|
+
* consumer installed, the dev copy's explains this repository's own tooling.
|
|
16
|
+
* A gate comparing whole diffs would fire on every unrelated PR — which is
|
|
17
|
+
* exactly the risk this roadmap's own register names at rank 4.
|
|
18
|
+
*
|
|
19
|
+
* ── Two modes, and why not one ──────────────────────────────────────────────
|
|
20
|
+
* `exact` — the non-comment diff must be EMPTY. This is the real check, and a
|
|
21
|
+
* twin is put here whenever it can be driven to zero.
|
|
22
|
+
*
|
|
23
|
+
* `bounded` — a recorded ceiling on non-comment changed lines, shrink-only. It
|
|
24
|
+
* exists for the twins that CANNOT reach zero, and the reason is structural
|
|
25
|
+
* rather than a preference: the installed consumer tree has no `scripts/_lib/`,
|
|
26
|
+
* so a template script may import node built-ins, `yaml` and its own installed
|
|
27
|
+
* siblings — nothing else. Every `keep-duplicated` verdict in the config names
|
|
28
|
+
* which non-installable import forces it.
|
|
29
|
+
*
|
|
30
|
+
* **What `bounded` does NOT claim**, stated because a ceiling reads stronger
|
|
31
|
+
* than it is: it catches GROWTH, not substitution. Replacing one 18-line
|
|
32
|
+
* divergence with a different 18-line divergence passes. That is the honest
|
|
33
|
+
* limit of a count, and it is why every twin that can be `exact` is `exact`.
|
|
34
|
+
*
|
|
35
|
+
* Exit codes: 0 within verdicts · 1 divergence beyond them · 2 config / IO error.
|
|
36
|
+
*
|
|
37
|
+
* Usage:
|
|
38
|
+
* ./scripts-run src/scripts/lint_memory_twin_parity
|
|
39
|
+
* ./scripts-run src/scripts/lint_memory_twin_parity --format json
|
|
40
|
+
* ./scripts-run src/scripts/lint_memory_twin_parity --root <fixture>
|
|
41
|
+
* ./scripts-run src/scripts/lint_memory_twin_parity --self-test
|
|
42
|
+
*/
|
|
43
|
+
import * as fs from 'node:fs';
|
|
44
|
+
import * as os from 'node:os';
|
|
45
|
+
import * as path from 'node:path';
|
|
46
|
+
import process from 'node:process';
|
|
47
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
48
|
+
|
|
49
|
+
import { parse as parseYaml } from 'yaml';
|
|
50
|
+
|
|
51
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
52
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
53
|
+
import { DeadScopeError, assertScanned } from './_lib/scan_scope.js';
|
|
54
|
+
|
|
55
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
56
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
57
|
+
|
|
58
|
+
export const CONFIG_REL = 'src/config/memory-twin-verdicts.yml';
|
|
59
|
+
|
|
60
|
+
export interface TwinVerdict {
|
|
61
|
+
file: string;
|
|
62
|
+
mode: 'exact' | 'bounded';
|
|
63
|
+
max_changed_lines?: number;
|
|
64
|
+
verdict: string;
|
|
65
|
+
reason: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface TwinConfig {
|
|
69
|
+
dev_root: string;
|
|
70
|
+
template_root: string;
|
|
71
|
+
twins: TwinVerdict[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function loadConfig(repoRoot: string): TwinConfig {
|
|
75
|
+
const raw = fs.readFileSync(path.join(repoRoot, CONFIG_REL), 'utf8');
|
|
76
|
+
const doc = parseYaml(raw) as Partial<TwinConfig>;
|
|
77
|
+
if (typeof doc.dev_root !== 'string' || typeof doc.template_root !== 'string' || !Array.isArray(doc.twins)) {
|
|
78
|
+
throw new Error(`${CONFIG_REL}: dev_root, template_root and twins are all required`);
|
|
79
|
+
}
|
|
80
|
+
return { dev_root: doc.dev_root, template_root: doc.template_root, twins: doc.twins };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Strip comments and blank lines.
|
|
85
|
+
*
|
|
86
|
+
* Deliberately textual rather than a parse: the gate must not need a TypeScript
|
|
87
|
+
* compiler to answer "did these two files diverge", and a stripper that is
|
|
88
|
+
* slightly conservative (a `*` continuation line inside a string would be
|
|
89
|
+
* dropped) errs toward reporting FEWER differences, which cannot manufacture a
|
|
90
|
+
* failure. It is the same shape the roadmap's own Phase 1 measurements used.
|
|
91
|
+
*/
|
|
92
|
+
export function stripComments(src: string): string[] {
|
|
93
|
+
const noBlocks = src.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
94
|
+
const out: string[] = [];
|
|
95
|
+
for (const line of noBlocks.split('\n')) {
|
|
96
|
+
const t = line.trim();
|
|
97
|
+
if (t === '' || t.startsWith('//') || t.startsWith('*')) continue;
|
|
98
|
+
out.push(line.replace(/\s+$/, ''));
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Count changed lines between two stripped files.
|
|
105
|
+
*
|
|
106
|
+
* A multiset difference, not an LCS diff: a line present N times on one side and
|
|
107
|
+
* M on the other contributes |N-M|. That makes the number invariant to
|
|
108
|
+
* reordering, which is what we want — moving a function is not a divergence in
|
|
109
|
+
* behaviour, and a real divergence changes the line CONTENT.
|
|
110
|
+
*/
|
|
111
|
+
export function changedLineCount(devLines: readonly string[], tplLines: readonly string[]): number {
|
|
112
|
+
const count = (ls: readonly string[]): Map<string, number> => {
|
|
113
|
+
const m = new Map<string, number>();
|
|
114
|
+
for (const l of ls) m.set(l, (m.get(l) ?? 0) + 1);
|
|
115
|
+
return m;
|
|
116
|
+
};
|
|
117
|
+
const a = count(devLines);
|
|
118
|
+
const b = count(tplLines);
|
|
119
|
+
let delta = 0;
|
|
120
|
+
for (const k of new Set([...a.keys(), ...b.keys()])) {
|
|
121
|
+
delta += Math.abs((a.get(k) ?? 0) - (b.get(k) ?? 0));
|
|
122
|
+
}
|
|
123
|
+
return delta;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface Finding {
|
|
127
|
+
file: string;
|
|
128
|
+
measured: number;
|
|
129
|
+
allowed: number | 'exact';
|
|
130
|
+
detail: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface Report {
|
|
134
|
+
findings: Finding[];
|
|
135
|
+
scanned: number;
|
|
136
|
+
measurements: { file: string; measured: number; mode: string }[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function evaluate(repoRoot: string, ledger?: GateLedger): Report {
|
|
140
|
+
const cfg = loadConfig(repoRoot);
|
|
141
|
+
const findings: Finding[] = [];
|
|
142
|
+
const measurements: Report['measurements'] = [];
|
|
143
|
+
ledger?.plan(cfg.twins.map((t) => t.file));
|
|
144
|
+
|
|
145
|
+
for (const twin of cfg.twins) {
|
|
146
|
+
const dev = path.join(repoRoot, cfg.dev_root, twin.file);
|
|
147
|
+
const tpl = path.join(repoRoot, cfg.template_root, twin.file);
|
|
148
|
+
if (!fs.existsSync(dev) || !fs.existsSync(tpl)) {
|
|
149
|
+
// A declared twin that no longer exists on both sides is a CONFIG
|
|
150
|
+
// defect, not a silent skip: the verdict is now describing nothing.
|
|
151
|
+
findings.push({
|
|
152
|
+
file: twin.file,
|
|
153
|
+
measured: -1,
|
|
154
|
+
allowed: twin.mode === 'exact' ? 'exact' : (twin.max_changed_lines ?? 0),
|
|
155
|
+
detail: `declared twin is missing on one side (dev=${fs.existsSync(dev)}, template=${fs.existsSync(tpl)}) — the verdict describes nothing`,
|
|
156
|
+
});
|
|
157
|
+
ledger?.fail(twin.file, 'declared twin missing on one side');
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const measured = changedLineCount(
|
|
161
|
+
stripComments(fs.readFileSync(dev, 'utf8')),
|
|
162
|
+
stripComments(fs.readFileSync(tpl, 'utf8')),
|
|
163
|
+
);
|
|
164
|
+
measurements.push({ file: twin.file, measured, mode: twin.mode });
|
|
165
|
+
|
|
166
|
+
if (twin.mode === 'exact') {
|
|
167
|
+
if (measured !== 0) {
|
|
168
|
+
findings.push({
|
|
169
|
+
file: twin.file,
|
|
170
|
+
measured,
|
|
171
|
+
allowed: 'exact',
|
|
172
|
+
detail: `declared EXACT parity but the copies differ by ${measured} non-comment line(s)`,
|
|
173
|
+
});
|
|
174
|
+
ledger?.fail(twin.file, `${measured} line(s) against exact parity`);
|
|
175
|
+
} else {
|
|
176
|
+
ledger?.complete(twin.file);
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const cap = twin.max_changed_lines ?? 0;
|
|
181
|
+
if (measured > cap) {
|
|
182
|
+
findings.push({
|
|
183
|
+
file: twin.file,
|
|
184
|
+
measured,
|
|
185
|
+
allowed: cap,
|
|
186
|
+
detail:
|
|
187
|
+
`${measured} non-comment changed line(s) against a recorded ceiling of ${cap} — ` +
|
|
188
|
+
`${measured - cap} new. The ceiling is shrink-only: reconcile the divergence, or ` +
|
|
189
|
+
`record a verdict for it in ${CONFIG_REL}. Raising the number is a defect, not a fix.`,
|
|
190
|
+
});
|
|
191
|
+
ledger?.fail(twin.file, `${measured} > ${cap}`);
|
|
192
|
+
} else {
|
|
193
|
+
ledger?.complete(twin.file);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { findings, scanned: cfg.twins.length, measurements };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function selfTest(): number {
|
|
200
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'twin-selftest-'));
|
|
201
|
+
const build = (name: string, devBody: string, tplBody: string, cfgTwin: string): string => {
|
|
202
|
+
const root = path.join(tmp, name);
|
|
203
|
+
fs.mkdirSync(path.join(root, 'src', 'scripts'), { recursive: true });
|
|
204
|
+
fs.mkdirSync(path.join(root, 'src', 'agent-src', 'templates', 'scripts'), { recursive: true });
|
|
205
|
+
fs.mkdirSync(path.join(root, 'src', 'config'), { recursive: true });
|
|
206
|
+
fs.writeFileSync(path.join(root, 'src', 'scripts', 'twin.ts'), devBody, 'utf8');
|
|
207
|
+
fs.writeFileSync(path.join(root, 'src', 'agent-src', 'templates', 'scripts', 'twin.ts'), tplBody, 'utf8');
|
|
208
|
+
fs.writeFileSync(
|
|
209
|
+
path.join(root, 'src', 'config', 'memory-twin-verdicts.yml'),
|
|
210
|
+
`schema_version: memory-twin-verdicts-v1\ndev_root: src/scripts\ntemplate_root: src/agent-src/templates/scripts\ntwins:\n${cfgTwin}\n`,
|
|
211
|
+
'utf8',
|
|
212
|
+
);
|
|
213
|
+
return root;
|
|
214
|
+
};
|
|
215
|
+
const exactTwin = " - file: twin.ts\n mode: exact\n verdict: dev-side-correct\n reason: self-test";
|
|
216
|
+
const run = (root: string): number =>
|
|
217
|
+
runGateCli(REPO_ROOT, 'src/scripts/lint_memory_twin_parity.ts', ['--root', root, '--quiet'], REPO_ROOT);
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
return runSelfTest({
|
|
221
|
+
gate: 'lint_memory_twin_parity',
|
|
222
|
+
minCases: 4,
|
|
223
|
+
minRejectCases: 2,
|
|
224
|
+
cases: [
|
|
225
|
+
{
|
|
226
|
+
name: 'identical copies pass',
|
|
227
|
+
expect: 'accept',
|
|
228
|
+
run: () => run(build('same', 'const a = 1;\n', 'const a = 1;\n', exactTwin)),
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
// The verify clause's green half: a comment-only plant must NOT red.
|
|
232
|
+
name: 'a comment-only divergence passes',
|
|
233
|
+
expect: 'accept',
|
|
234
|
+
run: () =>
|
|
235
|
+
run(build('comment', 'const a = 1;\n', '// template-only note\nconst a = 1;\n', exactTwin)),
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
// The verify clause's red half: one behavioural line must red.
|
|
239
|
+
name: 'a one-line behavioural divergence is rejected',
|
|
240
|
+
expect: 'reject',
|
|
241
|
+
run: () => run(build('drift', 'const a = 1;\n', 'const a = 2;\n', exactTwin)),
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: 'a declared twin missing on one side is rejected',
|
|
245
|
+
expect: 'reject',
|
|
246
|
+
run: () => {
|
|
247
|
+
const root = build('missing', 'const a = 1;\n', 'const a = 1;\n', exactTwin);
|
|
248
|
+
fs.rmSync(path.join(root, 'src', 'agent-src', 'templates', 'scripts', 'twin.ts'));
|
|
249
|
+
return run(root);
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: 'a bounded twin at its ceiling passes and one line over is rejected',
|
|
254
|
+
expect: 'reject',
|
|
255
|
+
run: () =>
|
|
256
|
+
run(
|
|
257
|
+
build(
|
|
258
|
+
'bounded',
|
|
259
|
+
'const a = 1;\nconst b = 2;\n',
|
|
260
|
+
'const a = 9;\nconst b = 8;\n',
|
|
261
|
+
' - file: twin.ts\n mode: bounded\n max_changed_lines: 2\n verdict: keep-duplicated\n reason: self-test',
|
|
262
|
+
),
|
|
263
|
+
),
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
});
|
|
267
|
+
} finally {
|
|
268
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
273
|
+
if (argv.includes('-h') || argv.includes('--help')) {
|
|
274
|
+
process.stdout.write('usage: lint_memory_twin_parity [--format json] [--root DIR] [--quiet] [--self-test]\n');
|
|
275
|
+
return 0;
|
|
276
|
+
}
|
|
277
|
+
if (argv.includes('--self-test')) return selfTest();
|
|
278
|
+
|
|
279
|
+
const quiet = argv.includes('--quiet');
|
|
280
|
+
const asJson = argv.includes('--format') && argv[argv.indexOf('--format') + 1] === 'json';
|
|
281
|
+
const rootIdx = argv.indexOf('--root');
|
|
282
|
+
const root = rootIdx !== -1 && rootIdx + 1 < argv.length ? path.resolve(argv[rootIdx + 1] as string) : REPO_ROOT;
|
|
283
|
+
const isFixture = root !== REPO_ROOT;
|
|
284
|
+
|
|
285
|
+
let report: Report;
|
|
286
|
+
const ledger = isFixture ? undefined : new GateLedger('lint_memory_twin_parity');
|
|
287
|
+
try {
|
|
288
|
+
report = evaluate(root, ledger);
|
|
289
|
+
} catch (e) {
|
|
290
|
+
process.stderr.write(`❌ lint_memory_twin_parity: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
291
|
+
return 2;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
assertScanned({
|
|
296
|
+
gate: 'lint_memory_twin_parity',
|
|
297
|
+
scanned: report.scanned,
|
|
298
|
+
units: 'declared twin(s)',
|
|
299
|
+
roots: [CONFIG_REL],
|
|
300
|
+
});
|
|
301
|
+
} catch (e) {
|
|
302
|
+
if (e instanceof DeadScopeError) {
|
|
303
|
+
process.stderr.write(`❌ ${e.message}\n`);
|
|
304
|
+
return 2;
|
|
305
|
+
}
|
|
306
|
+
throw e;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (asJson) {
|
|
310
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
311
|
+
}
|
|
312
|
+
ledger?.report(quiet ? () => undefined : undefined);
|
|
313
|
+
process.stdout.write(`scanned: ${report.scanned}\n`);
|
|
314
|
+
|
|
315
|
+
if (report.findings.length > 0) {
|
|
316
|
+
for (const f of report.findings) {
|
|
317
|
+
process.stderr.write(` 🔴 ${f.file}: ${f.detail}\n`);
|
|
318
|
+
}
|
|
319
|
+
process.stderr.write(`❌ lint_memory_twin_parity: ${report.findings.length} twin(s) diverged beyond their verdict.\n`);
|
|
320
|
+
return 1;
|
|
321
|
+
}
|
|
322
|
+
if (!quiet) {
|
|
323
|
+
for (const m of report.measurements) {
|
|
324
|
+
process.stdout.write(` · ${m.file.padEnd(26)} ${String(m.measured).padStart(4)} line(s) [${m.mode}]\n`);
|
|
325
|
+
}
|
|
326
|
+
process.stdout.write(`✅ lint_memory_twin_parity: ${report.scanned} twin(s) within their recorded verdicts.\n`);
|
|
327
|
+
}
|
|
328
|
+
return 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* c8 ignore start */
|
|
332
|
+
if (process.argv[1] !== undefined && pathToFileURL(path.resolve(process.argv[1])).href === pathToFileURL(_HERE).href) {
|
|
333
|
+
process.exit(main());
|
|
334
|
+
}
|
|
335
|
+
/* c8 ignore stop */
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lint_metric_consumers — a metric with no consumer does not land.
|
|
3
|
+
*
|
|
4
|
+
* `road-to-experience-loop-broadening` step 0.3, whose rule is quoted from the
|
|
5
|
+
* roadmap verbatim: *"A metric with no consumer is telemetry decoration and
|
|
6
|
+
* should not land."* Every entry in `src/config/metric-registry.yml` declares
|
|
7
|
+
* three things, and this gate refuses one that does not:
|
|
8
|
+
*
|
|
9
|
+
* - `consumer` — who reads it;
|
|
10
|
+
* - `decision` — what it changes;
|
|
11
|
+
* - `absent` — what fails without it.
|
|
12
|
+
*
|
|
13
|
+
* ## Why the third field, when the first two look sufficient
|
|
14
|
+
*
|
|
15
|
+
* Because the first two are answerable for a metric nobody needs. "The report
|
|
16
|
+
* reads it" and "it informs the roadmap" are both true of a number whose
|
|
17
|
+
* deletion would change nothing, and that is the number this gate exists to
|
|
18
|
+
* catch. `absent` is the falsifiable one: if the honest answer is *nothing
|
|
19
|
+
* fails*, the entry cannot be written and the metric should not land.
|
|
20
|
+
*
|
|
21
|
+
* The tree's own worked example is the 0.27 % dispatch capture rate — a figure
|
|
22
|
+
* that existed for months before anyone could say what decision it fed.
|
|
23
|
+
*
|
|
24
|
+
* ## What this gate does NOT claim
|
|
25
|
+
*
|
|
26
|
+
* It checks SHAPE, not truth. It refuses a missing, empty, or boilerplate
|
|
27
|
+
* field; it cannot tell a real consumer from a plausible sentence, because that
|
|
28
|
+
* is a review judgement and no parser has it. The honest statement is that this
|
|
29
|
+
* makes the OMISSION impossible, not the answer true — and it is written here
|
|
30
|
+
* rather than left for someone to discover by trusting the gate further than it
|
|
31
|
+
* goes.
|
|
32
|
+
*
|
|
33
|
+
* Exit codes: 0 clean · 1 finding · 2 usage error.
|
|
34
|
+
*
|
|
35
|
+
* A CONFIG error — a missing, unparseable or `metrics:`-less registry — exits
|
|
36
|
+
* **1**, not 2, and the docstring said 2 (R2 finding 6). 1 is right: a registry
|
|
37
|
+
* the gate cannot read is a finding about the tree, which is what this gate
|
|
38
|
+
* reports; 2 is reserved for being invoked wrongly. The prose is corrected
|
|
39
|
+
* rather than the code, because changing the exit code would change what a
|
|
40
|
+
* caller distinguishes.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import fs from 'node:fs';
|
|
44
|
+
import os from 'node:os';
|
|
45
|
+
import path from 'node:path';
|
|
46
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
47
|
+
import { parse as parseYaml } from 'yaml';
|
|
48
|
+
|
|
49
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
50
|
+
import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
|
|
51
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
52
|
+
|
|
53
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
const REPO_ROOT = path.resolve(HERE, '..', '..');
|
|
55
|
+
const REGISTRY_REL = path.join('src', 'config', 'metric-registry.yml');
|
|
56
|
+
const SELF_REL = path.join('src', 'scripts', 'lint_metric_consumers.ts');
|
|
57
|
+
|
|
58
|
+
/** The three fields that make a metric landable, and the reason each exists. */
|
|
59
|
+
export const REQUIRED_FIELDS = ['consumer', 'decision', 'absent'] as const;
|
|
60
|
+
export type RequiredField = (typeof REQUIRED_FIELDS)[number];
|
|
61
|
+
|
|
62
|
+
/** Below this a field is a placeholder rather than an answer. */
|
|
63
|
+
export const MIN_FIELD_CHARS = 20;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Phrases that pass a length check and answer nothing.
|
|
67
|
+
*
|
|
68
|
+
* Deliberately short and deliberately literal. A long list would turn this into
|
|
69
|
+
* a vocabulary filter, which is not what the gate is for — these are the four
|
|
70
|
+
* shapes that showed up when the registry was seeded, each of which is a way of
|
|
71
|
+
* writing "I do not know" at sufficient length.
|
|
72
|
+
*/
|
|
73
|
+
export const BOILERPLATE = [
|
|
74
|
+
'tbd',
|
|
75
|
+
'todo',
|
|
76
|
+
'n/a',
|
|
77
|
+
'not applicable',
|
|
78
|
+
'to be determined',
|
|
79
|
+
'unknown',
|
|
80
|
+
'nothing',
|
|
81
|
+
'see above',
|
|
82
|
+
] as const;
|
|
83
|
+
|
|
84
|
+
export interface MetricEntry {
|
|
85
|
+
readonly id?: unknown;
|
|
86
|
+
readonly producer?: unknown;
|
|
87
|
+
readonly consumer?: unknown;
|
|
88
|
+
readonly decision?: unknown;
|
|
89
|
+
readonly absent?: unknown;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface Finding {
|
|
93
|
+
readonly id: string;
|
|
94
|
+
readonly field: string;
|
|
95
|
+
readonly reason: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalise(value: unknown): string {
|
|
99
|
+
return typeof value === 'string' ? value.trim().replace(/\s+/g, ' ') : '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Judge one entry. Pure, so the self-test can drive it with literals. */
|
|
103
|
+
export function findingsFor(entry: MetricEntry, index: number): Finding[] {
|
|
104
|
+
const out: Finding[] = [];
|
|
105
|
+
const id = normalise(entry.id) === '' ? `#${String(index + 1)} (no id)` : normalise(entry.id);
|
|
106
|
+
|
|
107
|
+
if (normalise(entry.id) === '') {
|
|
108
|
+
out.push({ id, field: 'id', reason: 'missing — an unnamed metric cannot be cited' });
|
|
109
|
+
}
|
|
110
|
+
if (normalise(entry.producer) === '') {
|
|
111
|
+
out.push({
|
|
112
|
+
id,
|
|
113
|
+
field: 'producer',
|
|
114
|
+
reason: 'missing — without it a reader cannot check the other three against anything',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
for (const field of REQUIRED_FIELDS) {
|
|
118
|
+
const value = normalise(entry[field]);
|
|
119
|
+
if (value === '') {
|
|
120
|
+
out.push({ id, field, reason: 'missing or empty' });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// Matched at a WORD BOUNDARY, not on an exact token or a token plus a
|
|
124
|
+
// space (R2 finding 1). `TBD:` and `TODO:` are the natural writing
|
|
125
|
+
// forms of exactly the placeholder class this refuses, and both were
|
|
126
|
+
// accepted — the filter caught only the one spelling nobody uses.
|
|
127
|
+
const lower = value.toLowerCase();
|
|
128
|
+
if (BOILERPLATE.some((b) => lower === b || new RegExp(`^${b}\\b`).test(lower))) {
|
|
129
|
+
out.push({ id, field, reason: `boilerplate ("${value.slice(0, 40)}") — a placeholder, not an answer` });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (value.length < MIN_FIELD_CHARS) {
|
|
133
|
+
out.push({
|
|
134
|
+
id,
|
|
135
|
+
field,
|
|
136
|
+
reason:
|
|
137
|
+
`${String(value.length)} chars, below the ${String(MIN_FIELD_CHARS)}-char floor — `
|
|
138
|
+
+ 'too short to name a consumer, a decision or a consequence',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface RegistryRead {
|
|
146
|
+
readonly entries: readonly MetricEntry[];
|
|
147
|
+
readonly error: string | null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function readRegistry(root: string): RegistryRead {
|
|
151
|
+
const target = path.join(root, REGISTRY_REL);
|
|
152
|
+
let raw: string;
|
|
153
|
+
try {
|
|
154
|
+
raw = fs.readFileSync(target, 'utf8');
|
|
155
|
+
} catch {
|
|
156
|
+
return { entries: [], error: `${REGISTRY_REL} is missing` };
|
|
157
|
+
}
|
|
158
|
+
let parsed: unknown;
|
|
159
|
+
try {
|
|
160
|
+
parsed = parseYaml(raw);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
return { entries: [], error: `${REGISTRY_REL} does not parse: ${(err as Error).message}` };
|
|
163
|
+
}
|
|
164
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
165
|
+
return { entries: [], error: `${REGISTRY_REL} is not a mapping` };
|
|
166
|
+
}
|
|
167
|
+
const metrics = (parsed as { metrics?: unknown }).metrics;
|
|
168
|
+
if (!Array.isArray(metrics)) {
|
|
169
|
+
return { entries: [], error: `${REGISTRY_REL} has no \`metrics:\` list` };
|
|
170
|
+
}
|
|
171
|
+
return { entries: metrics as MetricEntry[], error: null };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* -------------------------------------------------------------------------- */
|
|
175
|
+
/* Self-test */
|
|
176
|
+
/* -------------------------------------------------------------------------- */
|
|
177
|
+
|
|
178
|
+
function selfTest(): number {
|
|
179
|
+
const fixture = (yaml: string): number => {
|
|
180
|
+
const dir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'metric-reg-'));
|
|
181
|
+
try {
|
|
182
|
+
const abs = path.join(dir, REGISTRY_REL);
|
|
183
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
184
|
+
fs.writeFileSync(abs, yaml);
|
|
185
|
+
// No `git init` here (R2 finding 7): this gate reads one YAML file
|
|
186
|
+
// and asks git nothing, so seeding a repo was seven subprocess
|
|
187
|
+
// spawns buying nothing.
|
|
188
|
+
return runGateCli(REPO_ROOT, SELF_REL, ['--root', dir], dir);
|
|
189
|
+
} finally {
|
|
190
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const complete = `
|
|
195
|
+
metrics:
|
|
196
|
+
- id: a-real-metric
|
|
197
|
+
producer: src/scripts/some_gate.ts
|
|
198
|
+
consumer: the ratchet in check_some_gate, and the maintainer reading its expiry
|
|
199
|
+
decision: whether a change that raises the count may merge at all
|
|
200
|
+
absent: the count rises unnoticed and the debt grows with nothing reporting it
|
|
201
|
+
`;
|
|
202
|
+
|
|
203
|
+
const cases: SelfTestCase[] = [
|
|
204
|
+
{
|
|
205
|
+
name: 'a complete entry is accepted',
|
|
206
|
+
expect: 'accept',
|
|
207
|
+
run: () => fixture(complete),
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'a metric with NO consumer is refused — the rule the gate exists for',
|
|
211
|
+
expect: 'reject',
|
|
212
|
+
run: () =>
|
|
213
|
+
fixture(complete.replace(/ consumer: .*\n/, '')),
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: 'a metric with no `decision` is refused',
|
|
217
|
+
expect: 'reject',
|
|
218
|
+
run: () => fixture(complete.replace(/ decision: .*\n/, '')),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'a metric with no `absent` is refused — the falsifiable field',
|
|
222
|
+
expect: 'reject',
|
|
223
|
+
run: () => fixture(complete.replace(/ absent: .*\n/, '')),
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: 'a BOILERPLATE consumer is refused, not merely a missing one',
|
|
227
|
+
expect: 'reject',
|
|
228
|
+
run: () => fixture(complete.replace(/ consumer: .*\n/, ' consumer: TBD\n')),
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
// The value must be short AND not boilerplate, or the boilerplate
|
|
232
|
+
// branch `continue`s past it and MIN_FIELD_CHARS gets no coverage
|
|
233
|
+
// at all — which is what happened with `nothing much` (R2 finding
|
|
234
|
+
// 2), while the gate-coverage row and the roadmap's verify clause
|
|
235
|
+
// both lean on this self-test as the discrimination proof.
|
|
236
|
+
name: 'a too-short field is refused, on the LENGTH floor rather than the boilerplate list',
|
|
237
|
+
expect: 'reject',
|
|
238
|
+
run: () => fixture(complete.replace(/ absent: .*\n/, ' absent: it breaks\n')),
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'an EMPTY registry is refused — a gate that scans nothing exits green',
|
|
242
|
+
expect: 'reject',
|
|
243
|
+
run: () => fixture('metrics: []\n'),
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
return runSelfTest({
|
|
248
|
+
gate: 'lint_metric_consumers',
|
|
249
|
+
cases,
|
|
250
|
+
minCases: 7,
|
|
251
|
+
minRejectCases: 5,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* -------------------------------------------------------------------------- */
|
|
256
|
+
|
|
257
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
258
|
+
if (argv.includes('--self-test')) return selfTest();
|
|
259
|
+
|
|
260
|
+
const rootFlag = argv.indexOf('--root');
|
|
261
|
+
if (rootFlag >= 0 && (argv[rootFlag + 1] === undefined || argv[rootFlag + 1]?.startsWith('--'))) {
|
|
262
|
+
process.stderr.write('lint_metric_consumers: --root needs a directory\n');
|
|
263
|
+
return 2;
|
|
264
|
+
}
|
|
265
|
+
const root = rootFlag >= 0 ? (argv[rootFlag + 1] as string) : REPO_ROOT;
|
|
266
|
+
const quiet = argv.includes('--quiet');
|
|
267
|
+
|
|
268
|
+
const read = readRegistry(root);
|
|
269
|
+
if (read.error !== null) {
|
|
270
|
+
process.stderr.write(`❌ lint_metric_consumers: ${read.error}\n`);
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Publishes AND asserts in one call. No `allowEmpty` reason: an empty
|
|
275
|
+
// registry is not a clean bill of health here, it is a gate reading nothing
|
|
276
|
+
// — and this gate's whole subject is numbers that exist without a purpose.
|
|
277
|
+
try {
|
|
278
|
+
reportScanned({
|
|
279
|
+
gate: 'lint_metric_consumers',
|
|
280
|
+
scanned: read.entries.length,
|
|
281
|
+
units: 'metric(s)',
|
|
282
|
+
roots: [REGISTRY_REL],
|
|
283
|
+
});
|
|
284
|
+
} catch (err) {
|
|
285
|
+
process.stderr.write(`❌ ${(err as Error).message}\n`);
|
|
286
|
+
return 1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Per-target accounting (R2 finding 4). A NEW gate adopts a ledger or takes
|
|
290
|
+
// an exemption; this one has a real target population — the registry's
|
|
291
|
+
// entries — so there is nothing to exempt. The plan is built from the index
|
|
292
|
+
// rather than from the id, because an entry MISSING its id is one of the
|
|
293
|
+
// findings, and a ledger keyed on a value that may be absent could not
|
|
294
|
+
// account for exactly the target that failed.
|
|
295
|
+
const ledger = new GateLedger('lint_metric_consumers');
|
|
296
|
+
const targetOf = (i: number): string => `${REGISTRY_REL}#${String(i + 1)}`;
|
|
297
|
+
ledger.plan(read.entries.map((_e, i) => targetOf(i)));
|
|
298
|
+
|
|
299
|
+
const findings: Finding[] = [];
|
|
300
|
+
const seen = new Set<string>();
|
|
301
|
+
read.entries.forEach((entry, i) => {
|
|
302
|
+
// A null or non-object element is a FINDING, not a crash (R2 finding
|
|
303
|
+
// 5). `metrics: [null]` reached `normalise(entry.id)` and threw an
|
|
304
|
+
// unhandled TypeError out of a gate whose job is to report.
|
|
305
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
306
|
+
findings.push({
|
|
307
|
+
id: `#${String(i + 1)}`,
|
|
308
|
+
field: '(entry)',
|
|
309
|
+
reason: 'not a mapping — a metric entry must be a key/value block',
|
|
310
|
+
});
|
|
311
|
+
ledger.fail(targetOf(i), 'not a mapping');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const before = findings.length;
|
|
315
|
+
findings.push(...findingsFor(entry, i));
|
|
316
|
+
const id = normalise(entry.id);
|
|
317
|
+
if (id !== '' && seen.has(id)) {
|
|
318
|
+
findings.push({ id, field: 'id', reason: 'duplicate — two entries claim the same metric' });
|
|
319
|
+
}
|
|
320
|
+
if (id !== '') seen.add(id);
|
|
321
|
+
if (findings.length > before) ledger.fail(targetOf(i), findings[before]?.reason ?? 'finding');
|
|
322
|
+
else ledger.complete(targetOf(i));
|
|
323
|
+
});
|
|
324
|
+
ledger.report();
|
|
325
|
+
|
|
326
|
+
if (findings.length > 0) {
|
|
327
|
+
process.stderr.write(
|
|
328
|
+
`❌ lint_metric_consumers: ${String(findings.length)} finding(s) in ${REGISTRY_REL}\n`,
|
|
329
|
+
);
|
|
330
|
+
for (const f of findings) {
|
|
331
|
+
process.stderr.write(` · ${f.id} → ${f.field}: ${f.reason}\n`);
|
|
332
|
+
}
|
|
333
|
+
process.stderr.write(
|
|
334
|
+
'\n A metric with no consumer is telemetry decoration and should not land.\n'
|
|
335
|
+
+ ' `absent` is the falsifiable field: if nothing fails without the metric,\n'
|
|
336
|
+
+ ' the entry cannot honestly be written and the metric should not exist.\n',
|
|
337
|
+
);
|
|
338
|
+
return 1;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!quiet) {
|
|
342
|
+
process.stdout.write(
|
|
343
|
+
`✅ lint_metric_consumers: ${String(read.entries.length)} metric(s), each naming a `
|
|
344
|
+
+ 'consumer, a decision and what fails without it.\n',
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (process.argv[1] !== undefined) {
|
|
351
|
+
const invoked = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
352
|
+
if (invoked === import.meta.url) {
|
|
353
|
+
process.exitCode = main();
|
|
354
|
+
}
|
|
355
|
+
}
|