@piagent/platform 1.5.4 → 1.6.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/CHANGELOG.md +136 -0
- package/README.md +9 -8
- package/README.vi.md +9 -5
- package/adapters/web-frontend/profile.json +2 -4
- package/architecture/layers.json +18 -3
- package/benchmarks/deep-logic-v1/SPECIALIST_PROTOCOL.vi.md +122 -0
- package/benchmarks/deep-logic-v1/grade.mjs +272 -10
- package/benchmarks/deep-logic-v1/project/apps/admin/src/billing-summary.js +3 -0
- package/benchmarks/deep-logic-v1/project/benchmark-contract/public-smoke.test.js +166 -0
- package/benchmarks/deep-logic-v1/project/package.json +1 -1
- package/benchmarks/deep-logic-v1/project/packages/billing/src/close-period.js +3 -0
- package/benchmarks/deep-logic-v1/project/packages/billing/src/plan-timeline.js +3 -0
- package/benchmarks/deep-logic-v1/project/test/smoke.test.js +7 -1
- package/benchmarks/deep-logic-v1/prompts/budgeted-context-graph.md +2 -0
- package/benchmarks/deep-logic-v1/prompts/fair-dependency-scheduler.md +2 -0
- package/benchmarks/deep-logic-v1/prompts/layered-policy-resolution.md +2 -0
- package/benchmarks/deep-logic-v1/prompts/resumable-stream-assembly.md +3 -1
- package/benchmarks/deep-logic-v1/prompts/revision-event-reconciliation.md +2 -0
- package/benchmarks/deep-logic-v1/prompts/temporal-usage-billing-close.md +123 -0
- package/benchmarks/deep-logic-v1/prompts/transactional-config-merge.md +2 -0
- package/benchmarks/deep-logic-v1/suite.json +51 -3
- package/benchmarks/deep-logic-v1/variant.mjs +22 -1
- package/benchmarks/production-v1/spend-control.v1.json +68 -0
- package/benchmarks/production-v1/suite.json +42 -3
- package/catalog/capabilities.json +6 -6
- package/docs/command-reference-vietnamese.md +6 -6
- package/docs/context-engine.md +7 -1
- package/docs/distribution-standard.md +7 -7
- package/docs/en/architecture.md +1 -0
- package/docs/languages.json +4 -4
- package/docs/model-options.md +8 -5
- package/docs/operator-manual-vietnamese.md +13 -11
- package/docs/project-adapters.md +2 -2
- package/docs/publishing-for-teams.md +9 -0
- package/docs/quality-benchmark.md +271 -62
- package/docs/release-install-policy.md +10 -9
- package/docs/runtime-policy-design.md +4 -4
- package/docs/runtime-quality-baseline.md +13 -6
- package/docs/security-threat-model.md +2 -2
- package/docs/task-lifecycle-vietnamese.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs/usage-observability.md +7 -2
- package/docs/vi/architecture.md +1 -0
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/context-engine.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/index.html +1 -1
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +1 -1
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
- package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
- package/evals/fixtures/piagent-webui/session-live-state-v1.invalid.json +18 -0
- package/evals/fixtures/piagent-webui/session-live-state-v1.valid.json +27 -0
- package/evals/fs4-readiness-matrix.v1.json +2 -2
- package/evals/fs5-causal-arm.v1.json +2 -2
- package/evals/fs5-pilot-protocol.v1.json +2 -2
- package/evals/fs5-pilot-protocol.v4.json +6 -5
- package/evals/fs5-pilot-protocol.v5.json +7 -6
- package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
- package/package.json +3 -1
- package/packages/piagent-core/benchmark/benchmark-assurance.js +4 -1
- package/packages/piagent-core/benchmark/benchmark-bootstrap.js +91 -9
- package/packages/piagent-core/benchmark/benchmark-claim-restrictions.js +38 -7
- package/packages/piagent-core/benchmark/benchmark-cli.js +7 -7
- package/packages/piagent-core/benchmark/benchmark-codex.js +3 -1
- package/packages/piagent-core/benchmark/benchmark-comparison.js +271 -2
- package/packages/piagent-core/benchmark/benchmark-core.js +732 -661
- package/packages/piagent-core/benchmark/benchmark-execution-guard.js +7 -5
- package/packages/piagent-core/benchmark/benchmark-forensics.js +439 -4
- package/packages/piagent-core/benchmark/benchmark-host-readiness.js +639 -0
- package/packages/piagent-core/benchmark/benchmark-matrix.js +22 -4
- package/packages/piagent-core/benchmark/benchmark-normalized-cost.js +317 -0
- package/packages/piagent-core/benchmark/benchmark-preflight.js +3 -1
- package/packages/piagent-core/benchmark/benchmark-process.js +6 -5
- package/packages/piagent-core/benchmark/benchmark-provider-wire.js +126 -0
- package/packages/piagent-core/benchmark/benchmark-record-validation.js +183 -0
- package/packages/piagent-core/benchmark/benchmark-report.js +116 -12
- package/packages/piagent-core/benchmark/benchmark-resume-recovery.js +14 -0
- package/packages/piagent-core/benchmark/benchmark-runner-policy.js +26 -0
- package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +782 -0
- package/packages/piagent-core/benchmark/benchmark-statistics.js +64 -0
- package/packages/piagent-core/benchmark/benchmark-stop-policy.js +8 -5
- package/packages/piagent-core/benchmark/benchmark-suite-identity.js +31 -0
- package/packages/piagent-core/benchmark/benchmark-suite-runtime.js +13 -9
- package/packages/piagent-core/benchmark/benchmark-suite.js +128 -2
- package/packages/piagent-core/benchmark/benchmark-summary-support.js +376 -0
- package/packages/piagent-core/benchmark/benchmark-timing-diagnostics.js +971 -0
- package/packages/piagent-core/benchmark/benchmark-usage.js +311 -0
- package/packages/piagent-core/benchmark/benchmark-workflow.js +166 -0
- package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +1080 -0
- package/packages/piagent-core/extensions/acceptance-contract-semantics.js +83 -69
- package/packages/piagent-core/extensions/acceptance-error-classes.js +160 -0
- package/packages/piagent-core/extensions/acceptance-executable-evidence.js +12 -9
- package/packages/piagent-core/extensions/acceptance-receipt.js +34 -11
- package/packages/piagent-core/extensions/acceptance-state-machine-evidence.js +518 -0
- package/packages/piagent-core/extensions/acceptance-test-binding-evidence.js +60 -0
- package/packages/piagent-core/extensions/context-efficiency-metrics.js +302 -0
- package/packages/piagent-core/extensions/context-efficiency-report.js +167 -0
- package/packages/piagent-core/extensions/context-engine.js +148 -145
- package/packages/piagent-core/extensions/context-evidence.js +48 -0
- package/packages/piagent-core/extensions/context-import-links.js +296 -0
- package/packages/piagent-core/extensions/context-telemetry-integrity.js +65 -0
- package/packages/piagent-core/extensions/criterion-context-pack.js +422 -7
- package/packages/piagent-core/extensions/criterion-graph.js +78 -8
- package/packages/piagent-core/extensions/guard-types.ts +3 -0
- package/packages/piagent-core/extensions/javascript-regex-evidence.js +32 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +250 -43
- package/packages/piagent-core/extensions/policy-core.js +1 -1
- package/packages/piagent-core/extensions/task-contract-view.js +17 -3
- package/packages/piagent-core/extensions/task-lifecycle.js +7 -2
- package/packages/piagent-core/extensions/task-state.js +40 -134
- package/packages/piagent-core/extensions/verification-intelligence.js +3 -1
- package/packages/piagent-core/extensions/workspace-evidence-roots.js +157 -0
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +3 -0
- package/packages/piagent-core/runtime/context/context-delivery.ts +103 -0
- package/packages/piagent-core/runtime/context/context-delta-shadow.ts +53 -0
- package/packages/piagent-core/runtime/context/context-efficiency-format.ts +47 -0
- package/packages/piagent-core/runtime/context/context-evidence-qualification.ts +32 -0
- package/packages/piagent-core/runtime/context/prefix-telemetry.ts +47 -0
- package/packages/piagent-core/runtime/context/repository-memory-hints.ts +27 -0
- package/packages/piagent-core/runtime/hooks/agent-prompt-telemetry.ts +56 -0
- package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +162 -102
- package/packages/piagent-core/runtime/hooks/completion-hook.ts +5 -6
- package/packages/piagent-core/runtime/hooks/input-hook.ts +11 -1
- package/packages/piagent-core/runtime/hooks/session-hooks.ts +22 -2
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +7 -2
- package/packages/piagent-core/runtime/hooks/tool-result-content-guards.ts +95 -0
- package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +84 -102
- package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +9 -3
- package/packages/piagent-core/runtime/inspection/activity-recovery.ts +104 -0
- package/packages/piagent-core/runtime/inspection/approval-broker.ts +29 -5
- package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +1 -1
- package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +1 -1
- package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +53 -0
- package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +34 -6
- package/packages/piagent-core/runtime/model/authenticated-catalog.ts +17 -9
- package/packages/piagent-core/runtime/model/openai-codex-reasoning.ts +83 -0
- package/packages/piagent-core/runtime/model/provider-wire-fingerprint.ts +238 -0
- package/packages/piagent-core/runtime/orchestration/role-policy.ts +1 -1
- package/packages/piagent-core/runtime/product/activity-inspector.ts +7 -2
- package/packages/piagent-core/runtime/product/efficiency-metrics.ts +4 -2
- package/packages/piagent-core/runtime/quality/exact-output-contract.ts +2 -1
- package/packages/piagent-core/runtime/quality/performance-assurance-signals.ts +23 -0
- package/packages/piagent-core/runtime/quality/performance-assurance.ts +6 -2
- package/packages/piagent-core/runtime/recovery/edit-recovery-context.ts +113 -0
- package/packages/piagent-core/runtime/recovery/edit-recovery-delivery.ts +42 -0
- package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +55 -2
- package/packages/piagent-core/runtime/recovery/semantic-repair-runtime.ts +55 -2
- package/packages/piagent-core/runtime/registration/context-commands.ts +66 -5
- package/packages/piagent-core/runtime/registration/policy-tools.ts +46 -11
- package/packages/piagent-core/runtime/registration/task-completion-tools.ts +49 -16
- package/packages/piagent-core/runtime/registration/task-start-guidance.ts +163 -0
- package/packages/piagent-core/runtime/registration/task-start-retry-helpers.ts +37 -0
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +67 -62
- package/packages/piagent-core/runtime/session/runtime-state.ts +154 -2
- package/packages/piagent-core/runtime/session/system-prompt.ts +4 -4
- package/packages/piagent-core/runtime/tools/apply-patch-tool.ts +500 -0
- package/packages/piagent-core/runtime/trajectory/trajectory-runtime.ts +2 -1
- package/packages/piagent-core/runtime/workflows/task-intake.ts +43 -2
- package/packages/piagent-core/security/sensitive-data.js +13 -218
- package/packages/piagent-core/security/sensitive-project-file.js +42 -0
- package/packages/piagent-core/security/sensitive-source-expression.js +330 -0
- package/packages/piagent-core/security/sensitive-source-formats.js +394 -0
- package/packages/piagent-core/security/sensitive-text.js +234 -0
- package/packages/piagent-core/subagents/piagent-worker.md +1 -1
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-rD9WJKxs.js → MarkdownMessage-CxeZqTte.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/index-DynIMe8I.js +176 -0
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/gateway/gateway-events.ts +109 -5
- package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
- package/packages/piagent-webui/gateway/gateway-session-stream.ts +93 -7
- package/packages/piagent-webui/gateway/session-command-controller.ts +18 -3
- package/packages/piagent-webui/gateway/session-effective-options.ts +47 -0
- package/packages/piagent-webui/gateway/session-live-state.ts +21 -0
- package/packages/piagent-webui/gateway/session-operation-start.ts +5 -1
- package/packages/piagent-webui/gateway/session-operation-watchdog.ts +158 -0
- package/packages/piagent-webui/gateway/session-runtime-factory.ts +45 -0
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +139 -120
- package/packages/piagent-webui/server/loopback-server.ts +8 -0
- package/packages/piagent-webui/server/transcript-projection.ts +22 -4
- package/packages/piagent-webui/shared/text-visibility.ts +10 -0
- package/schemas/piagent-webui/catalog-v1.json +9 -0
- package/schemas/piagent-webui/gateway-protocol-v1.schema.json +53 -3
- package/schemas/piagent-webui/session-live-state-v1.schema.json +76 -0
- package/schemas/task-contract.schema.json +12 -1
- package/scripts/benchmark-runner-core.mjs +355 -359
- package/scripts/benchmark-runner-finalization.mjs +309 -0
- package/scripts/benchmark-runner-host-readiness.mjs +94 -0
- package/scripts/benchmark-runner-support.mjs +200 -0
- package/scripts/benchmark-session.mjs +95 -41
- package/scripts/configure-model-scope.sh +4 -3
- package/scripts/helper-evaluation.mjs +1 -1
- package/scripts/install-global.sh +2 -2
- package/scripts/pi-usage-history.mjs +37 -10
- package/scripts/piagent-dashboard.mjs +13 -1
- package/scripts/setup.sh +2 -2
- package/scripts/verify-local.sh +5 -1
- package/templates/global/settings.json +2 -1
- package/packages/piagent-webui/dist/client/assets/index-BZRFw1u6.js +0 -174
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { changedFileAcceptanceCorpus, emptyAcceptanceCorpus, isAcceptanceTestPath } from "./acceptance-language-adapters.js";
|
|
4
|
+
import { durableContextEvidenceEntries, hasDurableContextEvidence } from "./context-evidence.js";
|
|
4
5
|
import { matchesAnyPath, normalizePathCandidate } from "./policy-core.js";
|
|
5
6
|
import { baselineReturnRepresentationConflicts, returnRepresentationGuidance } from "./return-contract.js";
|
|
6
7
|
import { hasLengthPrefixedIdentityKey, tenantAssertionSignals } from "./tenant-contract.js";
|
|
@@ -103,23 +104,46 @@ function changedFilesRespectBoundaries(changedFiles, boundaryPatterns) {
|
|
|
103
104
|
|
|
104
105
|
export function inferAcceptanceObligations(text, changeMode = "source-change") {
|
|
105
106
|
const value = normalizedText(text);
|
|
107
|
+
const accessControlText = value
|
|
108
|
+
.replace(/`[^`\n]*`/g, " ")
|
|
109
|
+
.replace(/\b(?:[a-z0-9_.@-]+\/)+[a-z0-9_.@/-]+\b/g, " ");
|
|
106
110
|
const obligations = [];
|
|
107
111
|
if (changeMode === "read-only" || READ_ONLY_BOUNDARY.test(value)) {
|
|
108
112
|
obligations.push("read-only-evidence");
|
|
109
113
|
}
|
|
110
|
-
const
|
|
111
|
-
/\
|
|
114
|
+
const actorAccessControl = includesAny(accessControlText, [
|
|
115
|
+
/\b(?:admins?|roles?|owners?|users?|callers?|resources?)\b[\s\S]{0,80}\b(?:access|allow(?:ed)?|deny|denied|block(?:ed)?|forbid(?:den)?|manag(?:e|es|ed|ing))\b/,
|
|
116
|
+
/\b(?:access|allow(?:ed)?|deny|denied|block(?:ed)?|forbid(?:den)?|manag(?:e|es|ed|ing))\b[\s\S]{0,80}\b(?:admins?|roles?|owners?|users?|callers?|resources?)\b/
|
|
112
117
|
]);
|
|
113
|
-
const
|
|
118
|
+
const explicitAuthorization = includesAny(accessControlText, [
|
|
119
|
+
/\bauth(?:orization)?\b/, /\bunauthoriz(?:ed|ation)\b/, /\bpermission\b/
|
|
120
|
+
]) || actorAccessControl;
|
|
121
|
+
const ownerAccessControl = /\bowner\b/.test(accessControlText) && includesAny(accessControlText, [
|
|
114
122
|
/\busers?\b/, /\bresources?\b/, /\baccess\b/, /\bmanag(?:e|es|ed|ing)\b/, /\bauth(?:orization)?\b/, /\bpermission\b/, /\broles?\b/
|
|
115
123
|
]);
|
|
116
124
|
if (explicitAuthorization || ownerAccessControl) {
|
|
117
125
|
obligations.push("authorization-deny-case");
|
|
118
126
|
}
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
const tenantMentioned = includesAny(value, [/\btenants?\b/, /\bcross[- ]tenant\b/, /\bsame[- ]tenant\b/, /\btenantid\b/]);
|
|
128
|
+
const tenantStorage = tenantMentioned && includesAny(value, [
|
|
129
|
+
/\bcache\b/, /\bcache[- ]?key\b/, /\bstorage\b/, /\bcollision\b/, /\bentity\b/, /\bsame tuple\b/
|
|
130
|
+
]);
|
|
131
|
+
const strongAuthorization = includesAny(accessControlText, [
|
|
132
|
+
/\bauth(?:orization)?\b/, /\bunauthoriz(?:ed|ation)\b/, /\bpermission\b/
|
|
133
|
+
]);
|
|
134
|
+
const tenantActorAccess = tenantMentioned && actorAccessControl;
|
|
135
|
+
const explicitTenantAccessBoundary = includesAny(value, [
|
|
136
|
+
/\bcross[- ]tenant\b/, /\bsame[- ]tenant\b/, /\btenant boundary\b/,
|
|
137
|
+
/\btenantid\b[\s\S]{0,100}\b(?:equal|match|same non-empty|access|allow|deny|block|forbid)/,
|
|
138
|
+
/\b(?:access|allow|deny|block|forbid)[\s\S]{0,100}\btenantid\b/
|
|
139
|
+
]) && includesAny(value, [
|
|
140
|
+
/\baccess\b/, /\ballow(?:ed)?\b/, /\b(?:deny|denied)\b/, /\bblock(?:ed)?\b/, /\bforbid(?:den)?\b/,
|
|
141
|
+
/\bequal\b/, /\bmatch(?:es|ed|ing)?\b/, /\bsame non-empty\b/
|
|
142
|
+
]);
|
|
143
|
+
if (tenantMentioned) {
|
|
144
|
+
if (tenantStorage) {
|
|
121
145
|
obligations.push("tenant-storage-isolation");
|
|
122
|
-
} else {
|
|
146
|
+
} else if (strongAuthorization || ownerAccessControl || tenantActorAccess || explicitTenantAccessBoundary) {
|
|
123
147
|
obligations.push("tenant-boundary");
|
|
124
148
|
}
|
|
125
149
|
}
|
|
@@ -674,14 +698,14 @@ function evidenceForObligation(obligation, task, corpus, currentWorkingTreeDiges
|
|
|
674
698
|
if (semanticConflictReasons(obligation, task, corpus, criterion).length > 0) return undefined;
|
|
675
699
|
|
|
676
700
|
if (obligation === "read-only-evidence") {
|
|
701
|
+
const readOnlyEvidence = durableContextEvidenceEntries(task);
|
|
677
702
|
const readOnlyOk = task.changeMode === "read-only"
|
|
678
703
|
&& corpus.files.length === 0
|
|
679
|
-
&&
|
|
680
|
-
&& task.contextManifest.length > 0;
|
|
704
|
+
&& hasDurableContextEvidence(task);
|
|
681
705
|
if (readOnlyOk) return {
|
|
682
706
|
kind: "read-only-context",
|
|
683
707
|
summary: "Read-only context was observed and no project files changed.",
|
|
684
|
-
paths:
|
|
708
|
+
paths: readOnlyEvidence.map((item) => item.path).slice(0, 8)
|
|
685
709
|
};
|
|
686
710
|
if (task.changeMode === "source-change") {
|
|
687
711
|
const boundaryPatterns = uniqueStrings([...(task.outOfScope ?? []), ...(task.protectedPaths ?? [])]);
|
|
@@ -695,8 +719,7 @@ function evidenceForObligation(obligation, task, corpus, currentWorkingTreeDiges
|
|
|
695
719
|
&& READ_ONLY_BOUNDARY.test(normalizedText(combinedText))
|
|
696
720
|
&& changedFilesRespectBoundaries(corpus.files, boundaryPatterns)
|
|
697
721
|
&& (boundaryPatterns.length === 0 || pathBoundaryMentioned(combinedText, boundaryPatterns))
|
|
698
|
-
&&
|
|
699
|
-
&& task.contextManifest.length > 0;
|
|
722
|
+
&& hasDurableContextEvidence(task);
|
|
700
723
|
if (sourceChangeReadOnlyOk) {
|
|
701
724
|
return {
|
|
702
725
|
kind: "source-change-read-only-boundary",
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { rejectionStatementErrorClass } from "./acceptance-error-classes.js";
|
|
2
|
+
|
|
3
|
+
function escapeRegex(value) {
|
|
4
|
+
return String(value ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function balancedEnd(text, openIndex, opening = "(", closing = ")", ceiling = 8_000) {
|
|
8
|
+
if (text[openIndex] !== opening) return -1;
|
|
9
|
+
let depth = 0;
|
|
10
|
+
for (let index = openIndex; index < Math.min(text.length, openIndex + ceiling); index += 1) {
|
|
11
|
+
if (text[index] === opening) depth += 1;
|
|
12
|
+
else if (text[index] === closing && --depth === 0) return index + 1;
|
|
13
|
+
}
|
|
14
|
+
return -1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function braceDepthAt(text, offset) {
|
|
18
|
+
let depth = 0;
|
|
19
|
+
for (let index = 0; index < offset; index += 1) {
|
|
20
|
+
if (text[index] === "{") depth += 1;
|
|
21
|
+
else if (text[index] === "}") depth = Math.max(0, depth - 1);
|
|
22
|
+
}
|
|
23
|
+
return depth;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function priorUnconditionalExit(text, offset) {
|
|
27
|
+
for (const match of text.slice(0, offset).matchAll(/\b(?:return|throw)\b/g)) {
|
|
28
|
+
if (braceDepthAt(text, match.index) !== 0) continue;
|
|
29
|
+
const statementStart = Math.max(text.lastIndexOf(";", match.index - 1), text.lastIndexOf("}", match.index - 1)) + 1;
|
|
30
|
+
if (!text.slice(statementStart, match.index).trim()) return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function topLevelParts(text, delimiter) {
|
|
36
|
+
const parts = [];
|
|
37
|
+
let start = 0;
|
|
38
|
+
let parentheses = 0;
|
|
39
|
+
let brackets = 0;
|
|
40
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
41
|
+
if (text[index] === "(") parentheses += 1;
|
|
42
|
+
else if (text[index] === ")") parentheses = Math.max(0, parentheses - 1);
|
|
43
|
+
else if (text[index] === "[") brackets += 1;
|
|
44
|
+
else if (text[index] === "]") brackets = Math.max(0, brackets - 1);
|
|
45
|
+
else if (text[index] === delimiter && parentheses === 0 && brackets === 0) {
|
|
46
|
+
parts.push(text.slice(start, index));
|
|
47
|
+
start = index + 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
parts.push(text.slice(start));
|
|
51
|
+
return parts;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stripOuterParentheses(raw) {
|
|
55
|
+
let value = String(raw ?? "").trim();
|
|
56
|
+
while (value.startsWith("(") && balancedEnd(value, 0) === value.length) value = value.slice(1, -1).trim();
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function staticFalsyToken(raw) {
|
|
61
|
+
const value = stripOuterParentheses(raw);
|
|
62
|
+
return /^(?:false|null|undefined|0|__pi_(?:empty|whitespace)_string_literal__)$/i.test(value)
|
|
63
|
+
|| /^(?:true\s*={2,3}\s*false|false\s*={2,3}\s*true)$/i.test(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function staticPrimitiveComparison(raw) {
|
|
67
|
+
const match = stripOuterParentheses(raw).match(/^(-?\d+(?:\.\d+)?|true|false)\s*(===|==|!==|!=|<=|>=|<|>)\s*(-?\d+(?:\.\d+)?|true|false)$/i);
|
|
68
|
+
if (!match) return undefined;
|
|
69
|
+
const value = (token) => /^(?:true|false)$/i.test(token) ? token.toLowerCase() === "true" : Number(token);
|
|
70
|
+
const left = value(match[1]), right = value(match[3]);
|
|
71
|
+
if (["===", "=="].includes(match[2])) return left === right;
|
|
72
|
+
if (["!==", "!="].includes(match[2])) return left !== right;
|
|
73
|
+
if (match[2] === "<") return left < right;
|
|
74
|
+
if (match[2] === ">") return left > right;
|
|
75
|
+
if (match[2] === "<=") return left <= right;
|
|
76
|
+
return left >= right;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function staticallyFalseCondition(raw) {
|
|
80
|
+
const value = stripOuterParentheses(raw);
|
|
81
|
+
return staticFalsyToken(value) || staticPrimitiveComparison(value) === false
|
|
82
|
+
|| value.replace(/[()]/g, " ").split("&&").some((part) => staticFalsyToken(part) || staticPrimitiveComparison(part) === false);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function staticallyTrueCondition(raw) {
|
|
86
|
+
const value = stripOuterParentheses(raw);
|
|
87
|
+
return /^true$/i.test(value) || staticPrimitiveComparison(value) === true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sideEffectFreeCondition(raw) {
|
|
91
|
+
const value = String(raw ?? "");
|
|
92
|
+
return !staticallyFalseCondition(value)
|
|
93
|
+
&& !/(?<![=!<>])=(?!=|>)|\+\+|--|=>|[,;?]|\b(?:await|delete|new|yield)\b/i.test(value)
|
|
94
|
+
&& !/\b[a-z_$][a-z0-9_$]*\s*\(/i.test(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function previousSignificantCharacter(text, offset) {
|
|
98
|
+
let index = offset - 1;
|
|
99
|
+
while (index >= 0 && /\s/.test(text[index])) index -= 1;
|
|
100
|
+
return text[index] ?? "";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function directBindingMutations(text, name) {
|
|
104
|
+
const escaped = escapeRegex(name);
|
|
105
|
+
const mutations = [];
|
|
106
|
+
const suffix = new RegExp(`(?<![a-z0-9_$])${escaped}\\b\\s*(?:=(?!=|>)|\\+\\+|--|(?:\\*\\*|<<|>>>?|&&|\\|\\||\\?\\?|[+*/%&|^-])=)`, "gi");
|
|
107
|
+
for (const match of text.matchAll(suffix)) {
|
|
108
|
+
const previous = previousSignificantCharacter(text, match.index);
|
|
109
|
+
if (match.index === undefined || previous && ".#".includes(previous)) continue;
|
|
110
|
+
mutations.push(match.index);
|
|
111
|
+
}
|
|
112
|
+
const prefix = new RegExp(`(?:\\+\\+|--)\\s*(?<![a-z0-9_$.?])${escaped}\\b`, "gi");
|
|
113
|
+
for (const match of text.matchAll(prefix)) mutations.push(match.index + match[0].toLowerCase().lastIndexOf(name.toLowerCase()));
|
|
114
|
+
const destructured = new RegExp(`(?:\\{[^}\\n]{0,500}\\b${escaped}\\b[^}\\n]{0,500}\\}|\\[[^\\]\\n]{0,500}\\b${escaped}\\b[^\\]\\n]{0,500}\\])\\s*=(?!=|>)|\\bfor(?:\\s+await)?\\s*\\(\\s*${escaped}\\s+(?:of|in)\\b`, "gi");
|
|
115
|
+
for (const match of text.matchAll(destructured)) mutations.push(match.index);
|
|
116
|
+
return [...new Set(mutations)].sort((left, right) => left - right);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function declarationInitializesAt(text, offset) {
|
|
120
|
+
return /\b(?:const|let|var)\s*$/i.test(text.slice(Math.max(0, offset - 120), offset));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function bindingWrittenBefore(text, name, offset) {
|
|
124
|
+
return directBindingMutations(text.slice(0, offset), name)
|
|
125
|
+
.some((mutation) => !declarationInitializesAt(text, mutation));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function bindingDeclared(text, name) {
|
|
129
|
+
const escaped = escapeRegex(name);
|
|
130
|
+
return new RegExp(`\\b(?:class|const|function|let|var)\\s+${escaped}\\b|\\b(?:const|let|var)\\s*[\\[{][^}\\]\\n]{0,500}\\b${escaped}\\b|\\bcatch\\s*\\([^)]*\\b${escaped}\\b|\\bimport\\b[^;\\n]{0,500}\\b${escaped}\\b`, "i").test(text);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function memberBindingMutation(text, name) {
|
|
134
|
+
const escaped = escapeRegex(name);
|
|
135
|
+
return new RegExp(`(?<![a-z0-9_$.?])${escaped}\\b\\s*(?:\\.[a-z_$][a-z0-9_$]*|\\[[^\\]]{1,300}\\])\\s*(?:=(?!=|>)|\\+\\+|--|(?:\\*\\*|<<|>>>?|&&|\\|\\||\\?\\?|[+*/%&|^-])=)`, "i").test(text);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function memberBindingCall(text, name) {
|
|
139
|
+
return new RegExp(`(?<![a-z0-9_$.?])${escapeRegex(name)}\\b\\s*(?:\\.[a-z_$][a-z0-9_$]*|\\[[^\\]]{1,300}\\])\\s*\\(`, "i").test(text);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function liveInputNames(text, names, referenceText, offset) {
|
|
143
|
+
return names.filter((name) => new RegExp(`\\b${escapeRegex(name)}\\b`, "i").test(referenceText)
|
|
144
|
+
&& !bindingWrittenBefore(text, name, offset));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function topLevelControlBefore(text, offset) {
|
|
148
|
+
for (const match of text.slice(0, offset).matchAll(/\b(?:do|for|if|return|switch|throw|try|while)\b/gi)) {
|
|
149
|
+
if (braceDepthAt(text, match.index) === 0) return true;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function topLevelControlBetween(text, start, end) {
|
|
155
|
+
const segment = text.slice(start, end);
|
|
156
|
+
for (const match of segment.matchAll(/\b(?:do|for|if|return|switch|throw|try|while)\b/gi)) {
|
|
157
|
+
if (braceDepthAt(segment, match.index) === 0) return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function preLoopStatementsProven(text, offset, carrier) {
|
|
163
|
+
let prefix = text.slice(0, offset), escaped = escapeRegex(carrier);
|
|
164
|
+
prefix = prefix.replace(new RegExp(`\\bconst\\s+${escaped}\\s*=\\s*string\\s*\\(\\s*[a-z_$][a-z0-9_$]*\\s*\\)\\s*(?:;|(?=\\r?\\n))`, "gi"), " ");
|
|
165
|
+
prefix = prefix.replace(/\b(?:let|var)\s+[a-z_$][a-z0-9_$]*(?:\s*:\s*(?:boolean|string))?\s*=\s*(?:false|__pi_empty_string_literal__)\s*(?:;|(?=\r?\n))/gi, " ");
|
|
166
|
+
return /^[\s;]*$/.test(prefix);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function trustedCarrier(text, carrier, offset, parameters, sourceCode) {
|
|
170
|
+
const normalized = carrier.toLowerCase();
|
|
171
|
+
const prefix = text.slice(0, offset);
|
|
172
|
+
if (parameters.includes(normalized)) return "";
|
|
173
|
+
const escaped = escapeRegex(normalized);
|
|
174
|
+
const declarations = [...prefix.matchAll(new RegExp(`\\bconst\\s+${escaped}\\s*=\\s*string\\s*\\(\\s*([a-z_$][a-z0-9_$]*)\\s*\\)\\s*(?:;|(?=\\r?\\n))`, "gi"))]
|
|
175
|
+
.filter((match) => braceDepthAt(prefix, match.index) === 0);
|
|
176
|
+
const input = declarations[0]?.[1]?.toLowerCase();
|
|
177
|
+
if (declarations.length !== 1 || !parameters.includes(input) || bindingDeclared(prefix.slice(0, declarations[0].index), input)
|
|
178
|
+
|| memberBindingCall(prefix.slice(0, declarations[0].index), input) || memberBindingMutation(prefix, input)
|
|
179
|
+
|| parameters.includes("string") || bindingDeclared(sourceCode, "string") || bindingWrittenBefore(sourceCode, "string", sourceCode.length)) return "";
|
|
180
|
+
return bindingWrittenBefore(prefix, normalized, prefix.length)
|
|
181
|
+
|| bindingWrittenBefore(prefix, input, prefix.length) || memberBindingMutation(prefix, normalized) ? "" : normalized;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function loopEvidence(text, header, offset, parameters, sourceCode) {
|
|
185
|
+
const clauses = topLevelParts(header, ";");
|
|
186
|
+
if (clauses.length !== 3) {
|
|
187
|
+
const iterator = header.match(/^\s*(?:const|let)\s+([a-z_$][a-z0-9_$]*)(?:\s*:\s*[^=;]+)?\s+of\s+([a-z_$][a-z0-9_$]*)\s*$/i);
|
|
188
|
+
const carrier = iterator && trustedCarrier(text, iterator[2], offset, parameters, sourceCode);
|
|
189
|
+
return carrier ? { carrier, counter: "", item: iterator[1].toLowerCase() } : undefined;
|
|
190
|
+
}
|
|
191
|
+
const initializer = stripOuterParentheses(clauses[0]).match(/^(?:let|var)\s+([a-z_$][a-z0-9_$]*)(?:\s*:\s*number)?\s*=\s*0$/i);
|
|
192
|
+
if (!initializer) return undefined;
|
|
193
|
+
const counter = initializer[1].toLowerCase(), escapedCounter = escapeRegex(counter);
|
|
194
|
+
const condition = stripOuterParentheses(clauses[1]);
|
|
195
|
+
const bound = condition.match(new RegExp(`^${escapedCounter}\\s*<\\s*([a-z_$][a-z0-9_$]*)\\s*\\.\\s*length$`, "i"));
|
|
196
|
+
const update = stripOuterParentheses(clauses[2]);
|
|
197
|
+
if (!bound || !new RegExp(`^(?:${escapedCounter}\\s*\\+\\+|\\+\\+\\s*${escapedCounter}|${escapedCounter}\\s*\\+=\\s*1)$`, "i").test(update)) return undefined;
|
|
198
|
+
const carrier = trustedCarrier(text, bound[1], offset, parameters, sourceCode);
|
|
199
|
+
return carrier ? { carrier, counter, item: "" } : undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function statementEnd(text, start, ceiling) {
|
|
203
|
+
const candidates = [text.indexOf(";", start), text.indexOf("\n", start), text.indexOf("\r", start)]
|
|
204
|
+
.filter((index) => index >= start && index < ceiling)
|
|
205
|
+
.map((index) => index + 1);
|
|
206
|
+
return candidates.length > 0 ? Math.min(...candidates) : ceiling;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function conditionalRanges(text, loop) {
|
|
210
|
+
const ranges = [];
|
|
211
|
+
for (const match of text.slice(loop.start, loop.end).matchAll(/\bif\s*\(/gi)) {
|
|
212
|
+
const conditionStart = loop.start + match.index;
|
|
213
|
+
const conditionOpen = text.indexOf("(", conditionStart);
|
|
214
|
+
const conditionEnd = balancedEnd(text, conditionOpen);
|
|
215
|
+
if (conditionEnd === -1 || conditionEnd > loop.end) continue;
|
|
216
|
+
const condition = text.slice(conditionOpen + 1, conditionEnd - 1);
|
|
217
|
+
let consequentStart = conditionEnd;
|
|
218
|
+
while (/\s/.test(text[consequentStart] ?? "")) consequentStart += 1;
|
|
219
|
+
const consequentEnd = text[consequentStart] === "{"
|
|
220
|
+
? balancedEnd(text, consequentStart, "{", "}")
|
|
221
|
+
: statementEnd(text, consequentStart, loop.end + 1);
|
|
222
|
+
if (consequentEnd > consequentStart && consequentEnd <= loop.end + 1) {
|
|
223
|
+
ranges.push({ conditionStart, condition, start: consequentStart, end: consequentEnd });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return ranges;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function exactBooleanWrite(text, offset, name) {
|
|
230
|
+
const match = text.slice(offset).match(new RegExp(`^${escapeRegex(name)}\\s*=\\s*(true|false)\\s*(?:;|(?=\\r?\\n|}))`, "i"));
|
|
231
|
+
return match ? { end: offset + match[0].length, value: match[1].toLowerCase() } : null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function branchHasPriorExit(text, start, offset) {
|
|
235
|
+
const branchStart = text[start] === "{" ? start + 1 : start;
|
|
236
|
+
const prefix = text.slice(branchStart, offset);
|
|
237
|
+
for (const match of prefix.matchAll(/\bif\s*\(/gi)) {
|
|
238
|
+
if (braceDepthAt(prefix, match.index) !== 0) continue;
|
|
239
|
+
const open = prefix.indexOf("(", match.index), end = balancedEnd(prefix, open);
|
|
240
|
+
if (end === -1 || !staticallyTrueCondition(prefix.slice(open + 1, end - 1))) continue;
|
|
241
|
+
const following = prefix.slice(end).match(/^\s*(?:\{\s*)?(?:break|continue|return|throw)\b/i);
|
|
242
|
+
if (following) return true;
|
|
243
|
+
}
|
|
244
|
+
for (const match of prefix.matchAll(/\b(?:break|continue|return|throw)\b/g)) {
|
|
245
|
+
if (braceDepthAt(prefix, match.index) !== 0) continue;
|
|
246
|
+
const statementStart = Math.max(prefix.lastIndexOf(";", match.index - 1), prefix.lastIndexOf("}", match.index - 1)) + 1;
|
|
247
|
+
if (!prefix.slice(statementStart, match.index).trim()) return true;
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function affirmativeQuotedTerminalContract(contractText, targetName) {
|
|
253
|
+
const normalized = String(contractText ?? "").normalize("NFD").replace(/[\u0300-\u036f`_*]/g, "").toLowerCase()
|
|
254
|
+
.replace(/\b(is|are|was|were|do|does|did|should|would|could|must|will|shall)n['’]t\b/g, "$1 not").replace(/\bcan(?:not|['’]t)\b/g, "can not").replace(/\bwon['’]t\b/g, "will not").replace(/\bshan['’]t\b/g, "shall not");
|
|
255
|
+
if (!targetName) return false;
|
|
256
|
+
const target = new RegExp(`\\b${escapeRegex(targetName)}\\b`, "i"), clauses = normalized.split(/(?<=[.!?;])\s+|\n+/);
|
|
257
|
+
const negated = (clause) => /\b(?:avoid|do\s+not|does\s+not|fails?\s+to|ignore|must\s+not|never|no|not|should\s+not|without|unchanged|unrelated|out\s+of\s+scope)\b/.test(clause);
|
|
258
|
+
const uniquelyMentionsTarget = (clause) => {
|
|
259
|
+
const calls = [...clause.matchAll(/\b([a-z_$][a-z0-9_$]*)\s*\(/gi)].map((match) => match[1].toLowerCase());
|
|
260
|
+
const escaped = escapeRegex(targetName), other = `(?!${escaped}\\b)[a-z_$][a-z0-9_$]*`, connector = `(?:,|and|or|with|alongside|rather\\s+than|instead\\s+of|versus|vs\\.?)`, peer = new RegExp(`(?:\\b${escaped}\\b(?:\\s*\\([^)]{0,200}\\))?\\s*${connector}\\s*${other}\\b|\\b${other}\\b\\s*${connector}\\s*\\b${escaped}\\b)`, "i");
|
|
261
|
+
return target.test(clause) && !peer.test(clause) && calls.every((name) => name === targetName.toLowerCase());
|
|
262
|
+
};
|
|
263
|
+
const directlyObligatesTarget = (clause) => uniquelyMentionsTarget(clause) && new RegExp(`\\b${escapeRegex(targetName)}\\b(?:\\s*\\([^)]{0,200}\\))?\\s+(?:(?:must|shall|should|will)\\s+)?(?:raise|reject|throw)s?\\b`, "i").test(clause);
|
|
264
|
+
const linkedTargetDirective = (clause) => new RegExp(`^(?:implement|preserve|replace|update)\\s+(?:the\\s+)?(?:(?:function|entrypoint)\\s+)?${escapeRegex(targetName)}(?:\\s*\\([^)]{0,200}\\))?(?:\\s+(?:function|entrypoint))?[.!?;]*$`, "i").test(clause.trim());
|
|
265
|
+
const linkedNeighbor = (index, affirmative) => {
|
|
266
|
+
const neighbors = [clauses[index - 1], clauses[index + 1]].filter((clause) => /^\s*(?:implement|preserve|replace|update)\b/i.test(clause ?? ""));
|
|
267
|
+
return /^\s*(?:(?:must|shall|should)\s+)?(?:raise|reject|throw)s?\b/i.test(affirmative)
|
|
268
|
+
&& neighbors.length === 1 && linkedTargetDirective(neighbors[0]) && !negated(neighbors[0]) && uniquelyMentionsTarget(neighbors[0]);
|
|
269
|
+
};
|
|
270
|
+
return clauses.some((clause, index) => (/\bunterminated\b[\s\S]{0,100}\bquoted?\s+field\b|\bquoted?\s+field\b[\s\S]{0,100}\bunterminated\b/.test(clause)
|
|
271
|
+
&& /\b(?:raise|reject|throw)s?\b[\s\S]{0,80}\bsyntaxerror\b|\bsyntaxerror\b[\s\S]{0,80}\b(?:raise|reject|throw)s?\b/.test(clause)
|
|
272
|
+
&& !negated(clause)
|
|
273
|
+
&& (directlyObligatesTarget(clause) || linkedNeighbor(index, clause))));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function quoteInputGuard(condition, loop) {
|
|
277
|
+
const operands = [...loop.items.map((name) => `\\b${escapeRegex(name)}\\b`)];
|
|
278
|
+
if (loop.counter) operands.push(`\\b${escapeRegex(loop.carrier)}\\b\\s*\\[\\s*${escapeRegex(loop.counter)}\\s*\\]`);
|
|
279
|
+
let quoteComparisons = 0;
|
|
280
|
+
for (const rawPart of topLevelParts(String(condition ?? "").replaceAll("&&", "\u0000"), "\u0000")) {
|
|
281
|
+
const part = stripOuterParentheses(rawPart);
|
|
282
|
+
const quote = operands.some((operand) => new RegExp(`^(?:${operand}\\s*={3}\\s*__pi_double_quote_string_literal__|__pi_double_quote_string_literal__\\s*={3}\\s*${operand})$`, "i").test(part));
|
|
283
|
+
if (quote) { quoteComparisons += 1; continue; }
|
|
284
|
+
const empty = part.match(/^(?:([a-z_$][a-z0-9_$]*)\s*={3}\s*__pi_empty_string_literal__|__pi_empty_string_literal__\s*={3}\s*([a-z_$][a-z0-9_$]*))$/i);
|
|
285
|
+
if (!empty || !loop.emptyAccumulators.includes((empty[1] ?? empty[2]).toLowerCase())) return false;
|
|
286
|
+
}
|
|
287
|
+
return quoteComparisons === 1;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function transitionEvidence(text, loop, ranges, state, item) {
|
|
291
|
+
const ancestors = ranges.filter((range) => item.offset >= range.start && item.write.end <= range.end);
|
|
292
|
+
if (ancestors.length === 0 || ancestors.some((range) => !sideEffectFreeCondition(range.condition)
|
|
293
|
+
|| branchHasPriorExit(text, range.start, item.offset)) || branchHasPriorExit(text, loop.start, item.offset)) return undefined;
|
|
294
|
+
if (!ancestors.some((range) => liveInputNames(text, loop.inputNames, range.condition, range.conditionStart).length > 0
|
|
295
|
+
&& quoteInputGuard(range.condition, loop))) return undefined;
|
|
296
|
+
const stateGuard = ancestors.some((range) => new RegExp(`^${escapeRegex(state)}(?:\\s*={2,3}\\s*true)?$`, "i").test(stripOuterParentheses(range.condition)));
|
|
297
|
+
const stateGuardBraced = ancestors.some((range) => text[range.start] === "{"
|
|
298
|
+
&& new RegExp(`^${escapeRegex(state)}(?:\\s*={2,3}\\s*true)?$`, "i").test(stripOuterParentheses(range.condition)));
|
|
299
|
+
const immediate = [...ancestors].sort((left, right) => (left.end - left.start) - (right.end - right.start))[0];
|
|
300
|
+
return { ...item, immediate, stateGuard, stateGuardBraced };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function initiallyFalseAuxiliaryGuard(text, loop, ranges, ancestors, state) {
|
|
304
|
+
for (const ancestor of ancestors) {
|
|
305
|
+
const auxiliary = stripOuterParentheses(ancestor.condition).match(/^([a-z_$][a-z0-9_$]*)(?:\s*={2,3}\s*true)?$/i)?.[1]?.toLowerCase();
|
|
306
|
+
if (!auxiliary || auxiliary === state || auxiliary === loop.counter || loop.inputNames.includes(auxiliary)) continue;
|
|
307
|
+
const declarations = [...text.slice(0, loop.start).matchAll(new RegExp(`\\b(?:let|var)\\s+${escapeRegex(auxiliary)}(?:\\s*:\\s*boolean)?\\s*=\\s*false\\s*(?:;|(?=\\r?\\n))`, "gi"))]
|
|
308
|
+
.filter((match) => braceDepthAt(text, match.index) === 0);
|
|
309
|
+
if (declarations.length !== 1) continue;
|
|
310
|
+
const mutations = directBindingMutations(text.slice(loop.start, ancestor.conditionStart), auxiliary);
|
|
311
|
+
if (mutations.every((relativeOffset) => {
|
|
312
|
+
const offset = loop.start + relativeOffset, write = exactBooleanWrite(text, offset, auxiliary);
|
|
313
|
+
if (!write) return false;
|
|
314
|
+
if (write.value === "false") return true;
|
|
315
|
+
const guards = ranges.filter((range) => offset >= range.start && write.end <= range.end);
|
|
316
|
+
return guards.some((range) => new RegExp(`^${escapeRegex(state)}(?:\\s*={2,3}\\s*true)?$`, "i").test(stripOuterParentheses(range.condition)))
|
|
317
|
+
&& guards.some((range) => quoteInputGuard(range.condition, loop));
|
|
318
|
+
})) return true;
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function counterFlowProven(text, loop, ranges, state, truthyOffset) {
|
|
324
|
+
if (!loop.counter) return true;
|
|
325
|
+
const bodyText = text.slice(loop.start, loop.end);
|
|
326
|
+
for (const relativeOffset of directBindingMutations(bodyText, loop.counter)) {
|
|
327
|
+
const offset = loop.start + relativeOffset;
|
|
328
|
+
if (!new RegExp(`^${escapeRegex(loop.counter)}\\s*(?:\\+\\+|\\+=\\s*1)\\s*(?:;|(?=\\r?\\n|}))`, "i").test(text.slice(offset))) return false;
|
|
329
|
+
if (offset >= truthyOffset) continue;
|
|
330
|
+
const ancestors = ranges.filter((range) => offset >= range.start && offset < range.end);
|
|
331
|
+
const stateGuard = ancestors.some((range) => new RegExp(`^${escapeRegex(state)}(?:\\s*={2,3}\\s*true)?$`, "i").test(stripOuterParentheses(range.condition)));
|
|
332
|
+
const nextQuote = ancestors.some((range) => new RegExp(`\\b${escapeRegex(loop.carrier)}\\b\\s*\\[\\s*${escapeRegex(loop.counter)}\\s*\\+\\s*1\\s*\\]\\s*={3}\\s*__pi_double_quote_string_literal__`, "i").test(range.condition));
|
|
333
|
+
if ((!stateGuard || !nextQuote) && !initiallyFalseAuxiliaryGuard(text, loop, ranges, ancestors, state)) return false;
|
|
334
|
+
}
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function priorSiblingControlProven(text, loop, ranges, state, truthyOffset) {
|
|
339
|
+
const loopText = text.slice(loop.start, loop.end), statePattern = new RegExp(`^${escapeRegex(state)}(?:\\s*={2,3}\\s*true)?$`, "i");
|
|
340
|
+
for (const range of ranges.filter((item) => item.conditionStart < truthyOffset
|
|
341
|
+
&& braceDepthAt(loopText, item.conditionStart - loop.start) === 0)) {
|
|
342
|
+
if (truthyOffset >= range.start && truthyOffset < range.end) continue;
|
|
343
|
+
if (text[range.start] === "{" && statePattern.test(stripOuterParentheses(range.condition))
|
|
344
|
+
&& !/^\s*else\b/i.test(text.slice(range.end))) continue;
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function statementFragmentProven(raw, loop) {
|
|
351
|
+
let value = String(raw ?? "").trim();
|
|
352
|
+
if (!value) return true;
|
|
353
|
+
if (/^else\b/i.test(value)) value = value.replace(/^else\b\s*/i, "");
|
|
354
|
+
while (/^if\s*\(/i.test(value)) {
|
|
355
|
+
const open = value.indexOf("("), end = balancedEnd(value, open);
|
|
356
|
+
if (end === -1) return false;
|
|
357
|
+
value = value.slice(end).trim();
|
|
358
|
+
}
|
|
359
|
+
const carrierItem = loop.counter ? `${escapeRegex(loop.carrier)}\\s*\\[\\s*${escapeRegex(loop.counter)}\\s*\\]` : "";
|
|
360
|
+
const inputs = [...loop.items.map(escapeRegex), carrierItem].filter(Boolean).join("|");
|
|
361
|
+
if (/^continue$/i.test(value)) return true;
|
|
362
|
+
if (loop.counter && new RegExp(`^(?:${escapeRegex(loop.counter)}\\s*(?:\\+\\+|\\+=\\s*1)|\\+\\+\\s*${escapeRegex(loop.counter)})$`, "i").test(value)) return true;
|
|
363
|
+
if (loop.booleanBindings.some((name) => new RegExp(`^${escapeRegex(name)}\\s*=\\s*(?:true|false)$`, "i").test(value))) return true;
|
|
364
|
+
if (inputs && loop.emptyAccumulators.some((name) => new RegExp(`^${escapeRegex(name)}\\s*\\+=\\s*(?:${inputs})$`, "i").test(value))) return true;
|
|
365
|
+
return Boolean(loop.counter && loop.items.some((name) => new RegExp(`^const\\s+${escapeRegex(name)}\\s*=\\s*${carrierItem}$`, "i").test(value)));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function loopStatementsProven(loopBody, loop) {
|
|
369
|
+
if (/\b(?:break|delete|new|return|throw|typeof|void)\b/i.test(loopBody)) return false;
|
|
370
|
+
if (/\b(?!if\b)[a-z_$][a-z0-9_$]*\s*(?:\?\.\s*)?\(|\b[a-z_$][a-z0-9_$]*\s*__pi_template_literal__|[\])]\s*(?:\?\.\s*)?\(/i.test(loopBody)) return false;
|
|
371
|
+
for (const match of loopBody.matchAll(/;/g)) {
|
|
372
|
+
const start = Math.max(loopBody.lastIndexOf(";", match.index - 1), loopBody.lastIndexOf("{", match.index - 1), loopBody.lastIndexOf("}", match.index - 1)) + 1;
|
|
373
|
+
if (!statementFragmentProven(loopBody.slice(start, match.index), loop)) return false;
|
|
374
|
+
}
|
|
375
|
+
for (const line of loopBody.split(/\r?\n/)) {
|
|
376
|
+
const tail = line.slice(line.lastIndexOf(";") + 1).trim();
|
|
377
|
+
if (!tail || /^[{}]*\s*(?:else\s*)?[{}]*$/i.test(tail)) continue;
|
|
378
|
+
if (/^(?:}\s*)?(?:else\s+)?if\s*\(/i.test(tail)) {
|
|
379
|
+
const open = tail.indexOf("("), end = balancedEnd(tail, open);
|
|
380
|
+
if (end !== -1 && /^[\s{}]*$/.test(tail.slice(end))) continue;
|
|
381
|
+
}
|
|
382
|
+
if (!statementFragmentProven(tail.replace(/^}\s*/, ""), loop)) return false;
|
|
383
|
+
}
|
|
384
|
+
for (const declaration of loopBody.matchAll(/\b(?:const|let|var)\b/gi)) {
|
|
385
|
+
const exactItem = loop.counter && loopBody.slice(declaration.index).match(new RegExp(`^const\\s+[a-z_$][a-z0-9_$]*\\s*=\\s*${escapeRegex(loop.carrier)}\\s*\\[\\s*${escapeRegex(loop.counter)}\\s*\\]\\s*(?:;|(?=\\r?\\n))`, "i"));
|
|
386
|
+
if (!exactItem || braceDepthAt(loopBody, declaration.index) !== 0) return false;
|
|
387
|
+
}
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function loopConditionsProven(ranges, loop, state) {
|
|
392
|
+
const statePattern = new RegExp(`^${escapeRegex(state)}(?:\\s*={2,3}\\s*true)?$`, "i");
|
|
393
|
+
const quote = "__pi_double_quote_string_literal__";
|
|
394
|
+
const next = loop.counter ? `${escapeRegex(loop.carrier)}\\s*\\[\\s*${escapeRegex(loop.counter)}\\s*\\+\\s*1\\s*\\]` : "";
|
|
395
|
+
return ranges.every((range) => {
|
|
396
|
+
const condition = stripOuterParentheses(range.condition);
|
|
397
|
+
if (!sideEffectFreeCondition(condition)) return false;
|
|
398
|
+
if (statePattern.test(condition) || quoteInputGuard(condition, loop)) return true;
|
|
399
|
+
if (next && new RegExp(`^(?:${next}\\s*={3}\\s*${quote}|${quote}\\s*={3}\\s*${next})$`, "i").test(condition)) return true;
|
|
400
|
+
return loop.booleanBindings.some((name) => name !== state && new RegExp(`^(?:!\\s*${escapeRegex(name)}|${escapeRegex(name)}(?:\\s*={2,3}\\s*(?:true|false))?)$`, "i").test(condition));
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function transitionShapeProven(text, transitions) {
|
|
405
|
+
const truthy = transitions.find((item) => item.write.value === "true");
|
|
406
|
+
const falsy = transitions.find((item) => item.write.value === "false");
|
|
407
|
+
if (!truthy || !falsy) return false;
|
|
408
|
+
if (falsy.offset < truthy.offset && falsy.stateGuardBraced && !truthy.stateGuard) return true;
|
|
409
|
+
if (truthy.offset >= falsy.offset || truthy.stateGuard || falsy.stateGuard) return false;
|
|
410
|
+
return /^\s*else\s*$/i.test(text.slice(truthy.immediate.end, falsy.immediate.conditionStart));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function statementConsequentEnd(text, start, ceiling) {
|
|
414
|
+
let offset = start;
|
|
415
|
+
while (/\s/.test(text[offset] ?? "")) offset += 1;
|
|
416
|
+
return text[offset] === "{" ? balancedEnd(text, offset, "{", "}") : statementEnd(text, offset, ceiling);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function elseChainEnd(text, start, ceiling) {
|
|
420
|
+
let offset = start;
|
|
421
|
+
while (offset < ceiling) {
|
|
422
|
+
while (/\s/.test(text[offset] ?? "")) offset += 1;
|
|
423
|
+
if (!/^else\b/i.test(text.slice(offset))) return offset;
|
|
424
|
+
offset += 4;
|
|
425
|
+
while (/\s/.test(text[offset] ?? "")) offset += 1;
|
|
426
|
+
if (/^if\b/i.test(text.slice(offset))) {
|
|
427
|
+
const open = text.indexOf("(", offset), end = balancedEnd(text, open);
|
|
428
|
+
if (open === -1 || end === -1 || end > ceiling) return -1;
|
|
429
|
+
offset = statementConsequentEnd(text, end, ceiling);
|
|
430
|
+
} else offset = statementConsequentEnd(text, offset, ceiling);
|
|
431
|
+
if (offset === -1 || offset > ceiling) return -1;
|
|
432
|
+
}
|
|
433
|
+
return offset;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function truthyTransitionTailProven(text, loop, transition) {
|
|
437
|
+
if (!/^[\s;}]*$/.test(text.slice(transition.write.end, transition.immediate.end))) return false;
|
|
438
|
+
const chainEnd = elseChainEnd(text, transition.immediate.end, loop.end);
|
|
439
|
+
return chainEnd !== -1 && /^[\s;]*$/.test(text.slice(chainEnd, loop.end));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Prove only one narrow terminal state-machine rejection shape. Callers must
|
|
443
|
+
// still establish stable exports/imports, live assertions, error constructors,
|
|
444
|
+
// the exact current verifier, and a current-tree changed source/test corpus.
|
|
445
|
+
export function statefulTerminalRejectionEvidence(body, parameters, requestedErrors, contractText, targetName, sourceCode) {
|
|
446
|
+
if (!affirmativeQuotedTerminalContract(contractText, targetName)) return false;
|
|
447
|
+
const loops = [];
|
|
448
|
+
for (const match of body.matchAll(/\bfor\s*\(/gi)) {
|
|
449
|
+
if (braceDepthAt(body, match.index) !== 0 || priorUnconditionalExit(body, match.index) || topLevelControlBefore(body, match.index)) continue;
|
|
450
|
+
const conditionOpen = body.indexOf("(", match.index), conditionEnd = balancedEnd(body, conditionOpen);
|
|
451
|
+
if (conditionEnd === -1) continue;
|
|
452
|
+
const header = body.slice(conditionOpen + 1, conditionEnd - 1);
|
|
453
|
+
const evidence = loopEvidence(body, header, match.index, parameters, sourceCode);
|
|
454
|
+
if (!evidence || !preLoopStatementsProven(body, match.index, evidence.carrier)) continue;
|
|
455
|
+
let bodyStart = conditionEnd;
|
|
456
|
+
while (/\s/.test(body[bodyStart] ?? "")) bodyStart += 1;
|
|
457
|
+
if (body[bodyStart] !== "{") continue;
|
|
458
|
+
const bodyEnd = balancedEnd(body, bodyStart, "{", "}");
|
|
459
|
+
const loopBody = body.slice(bodyStart + 1, bodyEnd - 1);
|
|
460
|
+
const nestedCallable = /=>|\b(?:catch|class|do|for|function|switch|try|while)\b|(?:^|[;{}]\s*)(?!(?:else|if)\b)[a-z_$][a-z0-9_$]*\s*\([^)]*\)\s*\{/im;
|
|
461
|
+
if (bodyEnd === -1 || nestedCallable.test(loopBody) || memberBindingMutation(loopBody, evidence.carrier)
|
|
462
|
+
|| [evidence.carrier, evidence.counter, evidence.item].filter(Boolean).some((name) => bindingDeclared(loopBody, name))) continue;
|
|
463
|
+
const items = [evidence.item].filter(Boolean), inputNames = [evidence.carrier, ...items];
|
|
464
|
+
if (evidence.counter) {
|
|
465
|
+
for (const declaration of loopBody.matchAll(new RegExp(`\\bconst\\s+([a-z_$][a-z0-9_$]*)\\s*=\\s*${escapeRegex(evidence.carrier)}\\s*\\[\\s*${escapeRegex(evidence.counter)}\\s*\\]\\s*(?:;|(?=\\r?\\n))`, "gi"))) {
|
|
466
|
+
if (braceDepthAt(loopBody, declaration.index) === 0) items.push(declaration[1].toLowerCase());
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
inputNames.push(...items);
|
|
470
|
+
const emptyAccumulators = [...body.slice(0, match.index).matchAll(/\b(?:let|var)\s+([a-z_$][a-z0-9_$]*)(?:\s*:\s*string)?\s*=\s*__pi_empty_string_literal__\s*(?:;|(?=\r?\n))/gi)]
|
|
471
|
+
.filter((item) => braceDepthAt(body, item.index) === 0).map((item) => item[1].toLowerCase());
|
|
472
|
+
const booleanBindings = [...body.slice(0, match.index).matchAll(/\b(?:let|var)\s+([a-z_$][a-z0-9_$]*)(?:\s*:\s*boolean)?\s*=\s*false\s*(?:;|(?=\r?\n))/gi)]
|
|
473
|
+
.filter((item) => braceDepthAt(body, item.index) === 0).map((item) => item[1].toLowerCase());
|
|
474
|
+
const loop = { ...evidence, start: bodyStart + 1, end: bodyEnd - 1, items: [...new Set(items)], inputNames: [...new Set(inputNames)], emptyAccumulators, booleanBindings };
|
|
475
|
+
if (loopStatementsProven(loopBody, loop)) loops.push(loop);
|
|
476
|
+
}
|
|
477
|
+
if (loops.length === 0) return false;
|
|
478
|
+
|
|
479
|
+
for (const declaration of body.matchAll(/\b(?:let|var)\s+([a-z_$][a-z0-9_$]*)(?:\s*:\s*boolean)?\s*=\s*false\s*(?:;|(?=\r?\n))/gi)) {
|
|
480
|
+
if (braceDepthAt(body, declaration.index) !== 0) continue;
|
|
481
|
+
const state = declaration[1].toLowerCase(), escaped = escapeRegex(state);
|
|
482
|
+
if (parameters.includes(state)
|
|
483
|
+
|| [...body.matchAll(new RegExp(`\\b(?:const|let|var)\\s+${escaped}\\b`, "gi"))].length !== 1) continue;
|
|
484
|
+
const mutations = directBindingMutations(body, state)
|
|
485
|
+
.filter((offset) => offset < declaration.index || offset >= declaration.index + declaration[0].length);
|
|
486
|
+
const writes = mutations.map((offset) => ({ offset, write: exactBooleanWrite(body, offset, state) }));
|
|
487
|
+
if (writes.length !== 2 || writes.some((item) => !item.write)
|
|
488
|
+
|| !writes.some((item) => item.write.value === "true") || !writes.some((item) => item.write.value === "false")) continue;
|
|
489
|
+
const loop = loops.find((candidate) => candidate.start > declaration.index + declaration[0].length
|
|
490
|
+
&& writes.every((item) => item.offset >= candidate.start && item.write.end <= candidate.end + 1));
|
|
491
|
+
if (!loop) continue;
|
|
492
|
+
const ranges = conditionalRanges(body, loop);
|
|
493
|
+
const transitions = writes.map((item) => transitionEvidence(body, loop, ranges, state, item));
|
|
494
|
+
const truthyOffset = writes.find((item) => item.write.value === "true").offset;
|
|
495
|
+
const truthyTransition = transitions.find((item) => item?.write.value === "true");
|
|
496
|
+
if (!loopConditionsProven(ranges, loop, state) || transitions.some((item) => !item) || !transitionShapeProven(body, transitions)
|
|
497
|
+
|| !counterFlowProven(body, loop, ranges, state, truthyOffset)
|
|
498
|
+
|| !priorSiblingControlProven(body, loop, ranges, state, truthyOffset)
|
|
499
|
+
|| !truthyTransitionTailProven(body, loop, truthyTransition)) continue;
|
|
500
|
+
|
|
501
|
+
for (const terminal of body.matchAll(/\bif\s*\(/gi)) {
|
|
502
|
+
if (terminal.index <= loop.end || braceDepthAt(body, terminal.index) !== 0 || priorUnconditionalExit(body, terminal.index)
|
|
503
|
+
|| topLevelControlBetween(body, loop.end + 1, terminal.index)
|
|
504
|
+
|| !/^[\s;]*$/.test(body.slice(loop.end + 1, terminal.index))) continue;
|
|
505
|
+
const conditionOpen = body.indexOf("(", terminal.index), conditionEnd = balancedEnd(body, conditionOpen);
|
|
506
|
+
if (conditionEnd === -1) continue;
|
|
507
|
+
const condition = stripOuterParentheses(body.slice(conditionOpen + 1, conditionEnd - 1));
|
|
508
|
+
if (!new RegExp(`^${escaped}(?:\\s*={2,3}\\s*true)?$`, "i").test(condition)) continue;
|
|
509
|
+
let consequentStart = conditionEnd;
|
|
510
|
+
while (/\s/.test(body[consequentStart] ?? "")) consequentStart += 1;
|
|
511
|
+
const consequentEnd = body[consequentStart] === "{"
|
|
512
|
+
? balancedEnd(body, consequentStart, "{", "}")
|
|
513
|
+
: statementEnd(body, consequentStart, body.length);
|
|
514
|
+
if (consequentEnd !== -1 && rejectionStatementErrorClass(body.slice(consequentStart, consequentEnd).replace(/^\s*\{/, ""), requestedErrors)) return true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return false;
|
|
518
|
+
}
|