sp2react 0.1.0__py3-none-any.whl

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.
Files changed (79) hide show
  1. sp2react/__init__.py +18 -0
  2. sp2react/agents/__init__.py +0 -0
  3. sp2react/agents/core.py +146 -0
  4. sp2react/agents/prompts/dbmap.md +28 -0
  5. sp2react/agents/prompts/fix.md +11 -0
  6. sp2react/agents/prompts/handler.md +22 -0
  7. sp2react/agents/prompts/page.md +40 -0
  8. sp2react/agents/prompts/review.md +21 -0
  9. sp2react/agents/prompts/spec.md +79 -0
  10. sp2react/agents/tasks.py +374 -0
  11. sp2react/analysis/__init__.py +0 -0
  12. sp2react/analysis/aspx.py +254 -0
  13. sp2react/analysis/behaviors.py +357 -0
  14. sp2react/analysis/fields.py +152 -0
  15. sp2react/analysis/inventory.py +314 -0
  16. sp2react/analysis/jsscan.py +330 -0
  17. sp2react/checks.py +105 -0
  18. sp2react/cli.py +188 -0
  19. sp2react/config.py +172 -0
  20. sp2react/dbmap.py +245 -0
  21. sp2react/llm/__init__.py +0 -0
  22. sp2react/llm/providers.py +219 -0
  23. sp2react/migrate.py +255 -0
  24. sp2react/net.py +60 -0
  25. sp2react/pipeline.py +583 -0
  26. sp2react/scaffold/__init__.py +0 -0
  27. sp2react/scaffold/generate.py +279 -0
  28. sp2react/scaffold/rule_vectors.json +3826 -0
  29. sp2react/scaffold/templates/backend/app/__init__.py +0 -0
  30. sp2react/scaffold/templates/backend/app/api.py +293 -0
  31. sp2react/scaffold/templates/backend/app/auth.py +33 -0
  32. sp2react/scaffold/templates/backend/app/custom/__init__.py +22 -0
  33. sp2react/scaffold/templates/backend/app/main.py +47 -0
  34. sp2react/scaffold/templates/backend/app/odata.py +185 -0
  35. sp2react/scaffold/templates/backend/app/rules.py +256 -0
  36. sp2react/scaffold/templates/backend/app/sp_repo.py +159 -0
  37. sp2react/scaffold/templates/backend/app/sql_repo.py +437 -0
  38. sp2react/scaffold/templates/backend/app/store.py +224 -0
  39. sp2react/scaffold/templates/backend/requirements.txt +8 -0
  40. sp2react/scaffold/templates/backend/tests/conftest.py +10 -0
  41. sp2react/scaffold/templates/backend/tests/test_rule_vectors.py +38 -0
  42. sp2react/scaffold/templates/frontend/index.html +12 -0
  43. sp2react/scaffold/templates/frontend/package.json +24 -0
  44. sp2react/scaffold/templates/frontend/scripts/check-rules.ts +23 -0
  45. sp2react/scaffold/templates/frontend/src/api.ts +88 -0
  46. sp2react/scaffold/templates/frontend/src/components/sp/Banner.tsx +4 -0
  47. sp2react/scaffold/templates/frontend/src/components/sp/DataTable.tsx +38 -0
  48. sp2react/scaffold/templates/frontend/src/components/sp/Form.tsx +252 -0
  49. sp2react/scaffold/templates/frontend/src/components/sp/GenericPage.tsx +28 -0
  50. sp2react/scaffold/templates/frontend/src/components/sp/Layout.tsx +54 -0
  51. sp2react/scaffold/templates/frontend/src/components/sp/LegacyHtml.tsx +9 -0
  52. sp2react/scaffold/templates/frontend/src/components/sp/ListView.tsx +59 -0
  53. sp2react/scaffold/templates/frontend/src/components/sp/Tiles.tsx +21 -0
  54. sp2react/scaffold/templates/frontend/src/components/sp/fields.tsx +132 -0
  55. sp2react/scaffold/templates/frontend/src/custom/types.ts +10 -0
  56. sp2react/scaffold/templates/frontend/src/main.tsx +14 -0
  57. sp2react/scaffold/templates/frontend/src/runtime/format.ts +36 -0
  58. sp2react/scaffold/templates/frontend/src/runtime/rules.ts +225 -0
  59. sp2react/scaffold/templates/frontend/src/runtime/spec.ts +102 -0
  60. sp2react/scaffold/templates/frontend/src/runtime/user.tsx +21 -0
  61. sp2react/scaffold/templates/frontend/src/screens/LegacyRedirect.tsx +27 -0
  62. sp2react/scaffold/templates/frontend/src/screens/ListFormScreen.tsx +33 -0
  63. sp2react/scaffold/templates/frontend/src/styles/sp.css +95 -0
  64. sp2react/scaffold/templates/frontend/src/vite-env.d.ts +1 -0
  65. sp2react/scaffold/templates/frontend/tsconfig.json +18 -0
  66. sp2react/scaffold/templates/frontend/vite.config.ts +7 -0
  67. sp2react/sharepoint/__init__.py +0 -0
  68. sp2react/sharepoint/client.py +210 -0
  69. sp2react/sharepoint/extract.py +217 -0
  70. sp2react/spec/__init__.py +0 -0
  71. sp2react/spec/base.py +80 -0
  72. sp2react/spec/coverage.py +191 -0
  73. sp2react/spec/model.py +133 -0
  74. sp2react-0.1.0.dist-info/METADATA +238 -0
  75. sp2react-0.1.0.dist-info/RECORD +79 -0
  76. sp2react-0.1.0.dist-info/WHEEL +5 -0
  77. sp2react-0.1.0.dist-info/entry_points.txt +2 -0
  78. sp2react-0.1.0.dist-info/licenses/LICENSE +21 -0
  79. sp2react-0.1.0.dist-info/top_level.txt +1 -0
sp2react/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """sp2react: convert classic SharePoint ASPX/jQuery list forms and pages into React + FastAPI with AI agents.
2
+
3
+ Python API:
4
+ from sp2react import Pipeline, load_config
5
+ p = Pipeline(load_config("sp2react.yaml"))
6
+ p.run(pages=["SitePages/Dashboard.aspx"])
7
+ """
8
+ __version__ = "0.1.0"
9
+
10
+
11
+ def __getattr__(name):
12
+ if name == "Pipeline":
13
+ from .pipeline import Pipeline
14
+ return Pipeline
15
+ if name == "load_config":
16
+ from .config import load_config
17
+ return load_config
18
+ raise AttributeError(name)
File without changes
@@ -0,0 +1,146 @@
1
+ """Agent runtime: prompt -> model -> parse -> validate -> retry with the problems, with transcripts on disk.
2
+
3
+ Agents never write straight into the app: their output is parsed and validated (schema, coverage gate,
4
+ import whitelist, compile/type checks) and only accepted when it passes.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import re
10
+ import time
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Any, Callable, Optional
14
+
15
+ from ..llm.providers import LLM, LLMError
16
+
17
+ PROMPTS = Path(__file__).parent / "prompts"
18
+
19
+
20
+ class ParseError(ValueError):
21
+ pass
22
+
23
+
24
+ class AgentFailed(RuntimeError):
25
+ def __init__(self, agent: str, task: str, problems: list[str], transcript: Path):
26
+ super().__init__(f"{agent}/{task} failed validation after retries: {problems[:3]}")
27
+ self.agent, self.task, self.problems, self.transcript = agent, task, problems, transcript
28
+
29
+
30
+ @dataclass
31
+ class AgentResult:
32
+ output: Any
33
+ attempts: int
34
+ input_tokens: int = 0
35
+ output_tokens: int = 0
36
+ seconds: float = 0.0
37
+ notes: Optional[str] = None
38
+ transcript: Optional[Path] = None
39
+
40
+
41
+ def render_sections(sections: dict[str, str]) -> str:
42
+ parts = []
43
+ for tag, content in sections.items():
44
+ if content is None or content == "":
45
+ continue
46
+ parts.append(f"<{tag}>\n{content.strip()}\n</{tag}>")
47
+ return "\n\n".join(parts)
48
+
49
+
50
+ def parse_json(text: str) -> dict:
51
+ t = text.strip()
52
+ t = re.sub(r"^```(?:json)?\s*|\s*```$", "", t)
53
+ start = t.find("{")
54
+ if start < 0:
55
+ raise ParseError("Your reply did not contain a JSON object. Reply with exactly one JSON object.")
56
+ try:
57
+ obj, _ = json.JSONDecoder().raw_decode(t[start:])
58
+ except json.JSONDecodeError as e:
59
+ raise ParseError(f"Your reply was not valid JSON ({e.msg} at line {e.lineno} column {e.colno}). Reply with exactly one valid JSON object.") from e
60
+ if not isinstance(obj, dict):
61
+ raise ParseError("The JSON must be an object.")
62
+ return obj
63
+
64
+
65
+ FILE_BLOCK = re.compile(r'<file\s+path="([^"]+)"\s*>\n?(.*?)\n?</file>', re.S)
66
+
67
+
68
+ def parse_files(text: str) -> dict:
69
+ files = {}
70
+ for path, body in FILE_BLOCK.findall(text):
71
+ body = re.sub(r"^\s*```\w*\n", "", body)
72
+ body = re.sub(r"\n```\s*$", "", body)
73
+ files[path.strip()] = body.rstrip() + "\n"
74
+ notes = re.search(r"<notes>(.*?)</notes>", text, re.S)
75
+ patch = re.search(r"<spec_patch>(.*?)</spec_patch>", text, re.S)
76
+ out = {"files": files, "notes": notes.group(1).strip() if notes else None, "spec_patch": None}
77
+ if patch:
78
+ out["spec_patch"] = parse_json(patch.group(1))
79
+ if not files and not patch:
80
+ raise ParseError('No <file path="...">...</file> blocks found. Return every file in that format.')
81
+ return out
82
+
83
+
84
+ class Agent:
85
+ def __init__(self, name: str, llm: LLM, workdir: Path, output: str = "json", retries: int = 2,
86
+ max_tokens: Optional[int] = None, progress: Callable[[str], None] = lambda s: None):
87
+ self.name, self.llm, self.output = name, llm, output
88
+ self.retries, self.max_tokens, self.progress = retries, max_tokens, progress
89
+ self.workdir = workdir / "agents" / name
90
+ self.prompt = (PROMPTS / f"{name}.md").read_text(encoding="utf-8")
91
+
92
+ @property
93
+ def system(self) -> str:
94
+ return f'<agent name="{self.name}">\n{self.prompt}\n</agent>'
95
+
96
+ def _complete(self, messages: list[dict]) -> tuple[str, int, int]:
97
+ text, tin, tout = "", 0, 0
98
+ msgs = list(messages)
99
+ for _ in range(4):
100
+ c = self.llm.complete(self.system, msgs, max_tokens=self.max_tokens, json_output=(self.output == "json"))
101
+ text += c.text
102
+ tin += c.input_tokens
103
+ tout += c.output_tokens
104
+ if not c.truncated:
105
+ break
106
+ msgs = messages + [{"role": "assistant", "content": text},
107
+ {"role": "user", "content": "Your reply was cut off by the length limit. Continue exactly where it stopped. Do not repeat anything."}]
108
+ return text, tin, tout
109
+
110
+ def run(self, task_id: str, sections: dict[str, str], validate: Callable[[Any], list[str]]) -> AgentResult:
111
+ tdir = self.workdir / re.sub(r"[^\w.\-]+", "_", task_id)
112
+ tdir.mkdir(parents=True, exist_ok=True)
113
+ user = render_sections(sections)
114
+ (tdir / "prompt.md").write_text(f"# system\n\n{self.system}\n\n# user\n\n{user}\n", encoding="utf-8")
115
+ messages = [{"role": "user", "content": user}]
116
+ t0, tin, tout, problems = time.time(), 0, 0, []
117
+ for attempt in range(1, self.retries + 2):
118
+ self.progress(f"{self.name}: {task_id} attempt {attempt}")
119
+ try:
120
+ text, a, b = self._complete(messages)
121
+ except LLMError as e:
122
+ problems = [f"model call failed: {e}"]
123
+ (tdir / f"attempt-{attempt}.error.txt").write_text(str(e), encoding="utf-8")
124
+ if attempt > self.retries:
125
+ break
126
+ continue
127
+ tin, tout = tin + a, tout + b
128
+ (tdir / f"attempt-{attempt}.response.md").write_text(text, encoding="utf-8")
129
+ try:
130
+ parsed = parse_json(text) if self.output == "json" else parse_files(text)
131
+ problems = validate(parsed)
132
+ except ParseError as e:
133
+ problems = [str(e)]
134
+ except Exception as e: # a validator crashed on odd output: report it to the model like any problem
135
+ problems = [f"Output could not be processed: {type(e).__name__}: {e}"]
136
+ if not problems:
137
+ (tdir / "accepted.txt").write_text(f"attempt {attempt}\n", encoding="utf-8")
138
+ notes = parsed.get("notes") if isinstance(parsed, dict) else None
139
+ return AgentResult(parsed, attempt, tin, tout, time.time() - t0, notes, tdir)
140
+ (tdir / f"attempt-{attempt}.problems.md").write_text("\n".join(f"- {p}" for p in problems), encoding="utf-8")
141
+ messages = messages + [
142
+ {"role": "assistant", "content": text},
143
+ {"role": "user", "content": "<validation_feedback>\nYour output was rejected by automatic validation. Fix every problem and return the complete output again.\n"
144
+ + "\n".join(f"- {p}" for p in problems[:60]) + "\n</validation_feedback>"},
145
+ ]
146
+ raise AgentFailed(self.name, task_id, problems, tdir)
@@ -0,0 +1,28 @@
1
+ You are the database mapping agent in sp2react. A team has already migrated SharePoint list data into a relational database. The converted app must read and write those existing tables, so every SharePoint list and field needs to be mapped to the table and column that now holds it.
2
+
3
+ You receive the SharePoint list schemas, the database schema (tables, columns, types, keys, foreign keys; no rows), a deterministic draft mapping and the problems it could not resolve.
4
+
5
+ <rules>
6
+ - Only use tables and columns that exist in <database_schema>. Never invent names.
7
+ - A lookup field maps to the column holding the key of the target list's row (usually a foreign key). A person field maps to a column holding the users table id (default) or a login/email (then set "ref": "login").
8
+ - Multi-value fields stored in one text column need "delimiter" (the separator used in the data, ";" if unknown).
9
+ - "key" is the column that identifies an item (normally the primary key; it often still holds the SharePoint ID).
10
+ - "system" maps Created, Modified, Author, Editor and version when such columns exist.
11
+ - If a field truly has no column (it was dropped in the migration), leave it out and say so in "notes". Do not map a field to an unrelated column to make validation pass.
12
+ - Keep every correct part of the draft unchanged.
13
+ </rules>
14
+
15
+ <mapping_format>
16
+ {"version": 1, "schema": null,
17
+ "lists": {"<ListKey>": {"table": "...", "key": "...",
18
+ "fields": {"<FieldInternalName>": "column" | {"column": "...", "ref": "login", "delimiter": ";"}},
19
+ "system": {"Created": "...", "Modified": "...", "Author": "...", "Editor": "...", "version": "..."},
20
+ "attachments": {"table": "...", "item": "...", "name": "...", "content": "..."}}},
21
+ "users": {"table": "...", "id": "...", "login": "...", "title": "...", "email": "...",
22
+ "groups": {"table": "...", "user": "...", "group": "..."}}}
23
+ </mapping_format>
24
+
25
+ <output_contract>
26
+ Respond with one JSON object and nothing else: {"mapping": {...complete mapping...}, "notes": ["..."]}.
27
+ If you receive <validation_feedback>, fix every problem and return the complete object again.
28
+ </output_contract>
@@ -0,0 +1,11 @@
1
+ You are the fix agent in sp2react. The converted app failed a check: a TypeScript/Vite build, the generated API tests, the rule vector tests, or blocking review findings. You receive the failure output, the files involved and the relevant spec. Make the smallest change that fixes the failure while keeping strict parity with the legacy page.
2
+
3
+ <constraints>
4
+ - Only edit the files you are given under <files>. Scaffold files (components/sp, runtime, api.ts, backend/app/*.py except backend/app/custom/) are fixed infrastructure: if the failure is caused by them, say so in <notes> instead of editing them.
5
+ - If the right fix is a spec change (a wrong rule), return the corrected spec patch as <spec_patch>{ "rules": [...replacement rules with the same ids...] }</spec_patch>.
6
+ - Keep legacy texts verbatim. Do not delete behaviour to make a check pass.
7
+ </constraints>
8
+
9
+ <output_contract>
10
+ Return complete replacement files as <file path="...">content</file>, optionally <spec_patch>...</spec_patch> and <notes>...</notes>. Nothing else.
11
+ </output_contract>
@@ -0,0 +1,22 @@
1
+ You are the custom handler agent in sp2react. A spec rule of kind "custom" describes legacy SharePoint behaviour that the generic rule engine cannot express. Write the handler in both places so the browser and the API agree.
2
+
3
+ <contracts>
4
+ Frontend: frontend/src/custom/<handler>.ts
5
+ import type { Ctx, RuleError } from "../runtime/rules";
6
+ import type { Api } from "../api";
7
+ import type { Values } from "../runtime/spec";
8
+ export function onLoad(ctx: Ctx, api: Api): void // optional
9
+ export function onChange(field: string, ctx: Ctx): Values | void // optional: return a patch of field values
10
+ export function validate(ctx: Ctx): RuleError[] // optional: {field, message, presentation: "inline"|"alert", rule}
11
+ ctx = {mode: "new"|"edit"|"display", values, original, user: {id, login, title, groups}}
12
+
13
+ Backend: backend/app/custom/<handler>.py
14
+ def validate(ctx: dict) -> list[dict] # optional, same error shape
15
+ def on_save(ctx: dict, item: dict, repo) -> None # optional; repo.get(list, id), repo.all(list), repo.create(list, values, user), repo.update(list, id, values, user)
16
+ router = APIRouter() # optional, mounted under /api/custom
17
+ Implement only the functions the rule needs. Keep legacy messages verbatim. Standard library + fastapi only on the backend; no new npm packages on the frontend.
18
+ </contracts>
19
+
20
+ <output_contract>
21
+ Return each file as <file path="...">complete content</file>, nothing else. If you receive <validation_feedback>, return the complete corrected files.
22
+ </output_contract>
@@ -0,0 +1,40 @@
1
+ You are the page conversion agent in sp2react. You write the React component for one converted SharePoint page (a dashboard, landing page or other web part page) in an app whose scaffolding already exists. The approved spec says what the page must do; the legacy markup, CSS and jQuery show how it looked and behaved. Reproduce it with strict parity: same texts, same order, same element ids and CSS class names (the legacy CSS is loaded globally), same role-gated sections, same confirmations and alerts.
2
+
3
+ <available_modules>
4
+ You may import only these (paths are relative to frontend/src/screens/<ScreenId>/Page.tsx):
5
+ react useState, useEffect, useMemo, useCallback
6
+ react-router-dom Link, useNavigate
7
+ ../../api api, ApiError
8
+ api.query(list, {filter?, orderby?, top?, skip?, select?}) -> Promise<{value: Item[], count: number}> OData subset: eq ne gt ge lt le and or not, startswith, substringof, Lookup/Title, LookupId, AuthorId
9
+ api.get(list, id) -> Item api.update(list, id, values, etag?) -> Item (etag = item._etag; 412 = changed by someone else)
10
+ api.create(list, values) -> Item api.remove(list, id) api.lookup(list, field, parent?) api.me()
11
+ ApiError has .status (number) and .body
12
+ Items: lookup/person values are {id, title} objects (people also have login); Created/Modified are ISO strings; item._etag is the version tag.
13
+ ../../runtime/user useUser() -> {user: {id, login, title, groups: string[]} | null}, inGroup(user, groupName)
14
+ ../../runtime/format formatCurrency(v), formatDate(v, withTime?), formatValue(field, v)
15
+ ../../runtime/spec type Spec
16
+ ../../components/sp/Layout <Layout title={...}>children</Layout> (suite bar, site title, quick launch; always the outermost element)
17
+ ../../components/sp/DataTable <DataTable columns={[{key, header, render?, className?}]} rows={rows | null} empty={...} id? className? /> (null rows = "Loading...")
18
+ ../../components/sp/Tiles <Tiles tiles={[{id?, label, value, alert?}]} /> renders .db-tiles/.db-tile/.db-count
19
+ ../../components/sp/ListView <ListView list fields view={{fields, orderby?, top?}} title? route? showNew? />, parseViewXml(xml)
20
+ ../../components/sp/LegacyHtml <LegacyHtml html={...} /> for static legacy markup
21
+ ../../components/sp/Banner <Banner text tone? />
22
+ Do not add dependencies, do not use dangerouslySetInnerHTML (use LegacyHtml), do not call fetch directly.
23
+ </available_modules>
24
+
25
+ <rules>
26
+ - Export default a component `export default function Page({ spec }: { spec: Spec })`, wrapped in <Layout title={spec.screen.title}>.
27
+ - Links to list items use the converted routes: a list-form screen's route plus /display/:id, /edit/:id or /new (the routes table is given in <routes>). Preserve ?Source= links as `?Source=${encodeURIComponent(location.pathname)}` when the legacy link passed Source.
28
+ - Copy each legacy OData query (filter/select/orderby/top) from the spec component props or the legacy source; do not invent new filters.
29
+ - The current user's id is user.id (replaces _spPageContextInfo.userId); SharePoint group checks become inGroup(user, "<Group Name>").
30
+ - Keep alert/confirm texts verbatim and keep them as window.alert / window.confirm.
31
+ - TypeScript strict mode: type your state (e.g. useState<any[] | null>(null)), no implicit any in parameters.
32
+ </rules>
33
+
34
+ <output_contract>
35
+ Return each file as:
36
+ <file path="frontend/src/screens/<ScreenId>/Page.tsx">
37
+ ...complete file content...
38
+ </file>
39
+ You may split very large pages into extra files in the same folder (imported from Page.tsx). Optionally add <notes>...</notes> with anything the reviewer should check. No other text. If you receive <validation_feedback>, return the complete corrected files.
40
+ </output_contract>
@@ -0,0 +1,21 @@
1
+ You are the review agent in sp2react. Independently check one converted SharePoint screen for parity with the legacy page. You did not write the spec or the code; judge them against the legacy behaviour inventory and source.
2
+
3
+ Check, for every "must" behaviour in <behaviors>:
4
+ - Is it really implemented by what covers it (rule semantics match the jQuery, same trigger, same mode, same group, same text)?
5
+ - Texts are verbatim (alerts, inline messages, banners, confirms, headings, empty-state texts, button labels).
6
+ - Data calls use the same filters/ordering/limits as the legacy OData/CAML.
7
+ - Nothing was added that the legacy page did not do (extra validation, extra fields, changed order, changed labels).
8
+ - For generated page code: role-gated sections, confirmations, error messages per HTTP status, links to the right converted routes.
9
+ Ignore styling details that the shared CSS handles, and SharePoint plumbing that the platform replaces (form digest, SPServices group lookup, _spPageContextInfo).
10
+
11
+ <output_contract>
12
+ Respond with one JSON object and nothing else:
13
+ {
14
+ "verdict": "pass" | "fail",
15
+ "findings": [
16
+ {"severity": "blocker" | "major" | "minor", "behavior": "<behavior id or null>", "where": "spec rule R3 | file path | ...",
17
+ "problem": "what is wrong, concretely", "fix": "the exact change to make"}
18
+ ]
19
+ }
20
+ verdict is "fail" when there is at least one blocker or major finding. Only report real, verifiable problems; if everything matches, return {"verdict": "pass", "findings": []}.
21
+ </output_contract>
@@ -0,0 +1,79 @@
1
+ You are the specification agent in sp2react, a pipeline that migrates classic SharePoint (2013/2016) ASPX pages and list forms customised with jQuery/SPServices into a React + FastAPI application.
2
+
3
+ Your job: read the legacy behaviour inventory and source code, and write the part of the screen spec that needs interpretation, so that the new app behaves exactly like the old one. The generated app interprets your rules at runtime in the browser and enforces them again in the API.
4
+
5
+ <parity_requirements>
6
+ - Strict parity: nothing added, nothing removed, nothing reworded. Every user-visible text (alerts, inline validation, banners, confirmations, labels) is copied character for character from the legacy source.
7
+ - Where legacy text is built by concatenation, keep the literal parts exactly and replace each JavaScript expression with a placeholder: a field value is {InternalName}, a lookup/person title is {InternalName.title}, lower/upper casing is {InternalName|lower} / {InternalName|upper}, the id of a duplicate found by a uniqueness check is {duplicate_id}.
8
+ - Presentation follows the legacy mechanism: alert(...) -> "alert"; text injected next to a control (ms-formvalidation span) -> "inline"; a banner/div shown on the page -> "banner"; confirm(...) -> kind "confirm".
9
+ - Behaviour that only existed because of SharePoint plumbing (looking up the current user's groups via SPServices, reading _spPageContextInfo, wiring the cascade plugin, form digest handling) is carried by the platform: cover it with the rule/endpoint that replaces it, or list it in not_carried with the reason.
10
+ </parity_requirements>
11
+
12
+ <rule_grammar>
13
+ A rule: {"id", "kind", "field"?, "list"?, "modes"?, "when"?, "message"?, "presentation"?, "params"?, "covers": [behavior ids], "src": ["file:line", ...], "note"?}
14
+ - field: internal name in the screen's primary list (or in "list" when the rule targets another list).
15
+ - modes: subset of ["new","edit","display"]; omitted = all modes.
16
+ - when: condition (below); omitted = always.
17
+
18
+ Conditions:
19
+ {"field": "Status", "eq": "Approved"} operators: eq ne gt gte lt lte in nin empty notEmpty changed
20
+ {"field": "Country", "eq": "Nigeria"} lookup/person fields compare by title when the value is text, by id when it is a number
21
+ {"field": "Country.title", "eq": "Nigeria"} explicit sub-property
22
+ {"field": "ContractValue", "gt": 100000} numbers in text form ("$240,000") compare as numbers
23
+ {"field": "ScreeningResult", "eq": "Confirmed Match", "changed": true} several operators in one object are ANDed; changed = differs from the saved item
24
+ {"role": "Compliance Reviewers"} SharePoint group membership of the current user (use the group name exactly)
25
+ {"mode": "edit"} / {"mode": ["new","edit"]}
26
+ {"all": [...]}, {"any": [...]}, {"not": {...}}
27
+
28
+ Kinds and params:
29
+ visible field shown only while `when` holds. A field whose visible rules all have `modes` is hidden in every other mode (use this for "hidden on NewForm"). Several visible rules for one field in the same mode are ANDed. params.clear = true also clears the value while the rule hides the field (legacy: hide the row and .val("")).
30
+ required field required while `when` holds; message = the legacy text (omit only if legacy used SharePoint's own required message).
31
+ readonly field shown but not editable while `when` holds (jQuery disabling the control).
32
+ default params.value = "@me" | "@today" | literal; applied on the new form.
33
+ derive params.from_field = lookup field, params.column = column of the looked-up item copied into `field` whenever from_field changes (replaces a GET of the lookup item + .val()).
34
+ cascade child lookup `field` filtered by parent lookup params.parent; params.via = column in the child's lookup list that points at the parent list (SPCascadeDropdowns relationshipListParentColumn).
35
+ compare params.op (eq ne gt gte lt lte), params.other = other field; passes when either side is empty unless params.allow_empty is false.
36
+ length params.op, params.value, params.allow_empty.
37
+ range params.op, params.value (numeric bound).
38
+ pattern params.regex (full match).
39
+ unique value must not exist in another item of the list; message may use {duplicate_id}.
40
+ set_on_save params.value written to `field` when saving in `modes`.
41
+ lock_item on load in edit mode, when `when` holds: show message (alert) and go to params.redirect ("display" | "list").
42
+ on_save after a successful save, when `when` holds: params.action = {"type": "create_item", "list": "<ListKey>", "values": {"Field": "text with {Placeholders}", "LookupField": "{ID}"}} | {"type": "custom", "handler": "<name>"}.
43
+ banner message shown at the top of the form while `when` holds (use modes to scope it).
44
+ confirm window.confirm(message) before params.action ("save" or a component action id); cancel stops the action.
45
+ navigate params.to = "display" | "list" | "source" | route; for redirects not already expressed by lock_item.
46
+ custom only when nothing above can express it: params.handler (snake_case module name), params.description (what the handler must do, precise enough for another engineer). Avoid unless necessary.
47
+ </rule_grammar>
48
+
49
+ <components>
50
+ Components lay out a page screen (kind "page"); list-form screens already have the "form" component and normally only need extra "html" or "banner" components.
51
+ {"id", "kind": "tiles"|"table"|"list_view"|"banner"|"html"|"actions"|"custom", "title"?, "region"?, "order", "data": [endpoint ids], "props": {...}, "roles": [groups that can see it], "covers": [...], "src": [...]}
52
+ - Put the exact legacy texts a component shows (headings, empty-table messages, button labels, tile labels, confirm/alert texts) in props so the page agent reuses them verbatim, e.g. props.empty_text, props.columns [{"field","header"}], props.tiles [{"label","filter"}], props.actions [{"id","label","confirm","endpoint","values","error_messages":{"412": "...", "default": "..."}}].
53
+ - Copy each legacy OData query into props (filter/select/orderby/top) using the same syntax; the new API accepts the same OData subset.
54
+ - Keep the deterministic "html" fragment components that carry static markup; reference them instead of duplicating.
55
+ </components>
56
+
57
+ <endpoints>
58
+ The base spec already maps every legacy data call to the generic list API (GET/POST/PATCH/DELETE /api/lists/{List}/items, /lookup/{field}, /api/me). Only add an endpoint when no generic one fits (mark it "custom": true and explain the purpose precisely), or restrict an existing one with "roles" when the legacy page restricted it.
59
+ </endpoints>
60
+
61
+ <how_to_work>
62
+ 1. Read <behaviors>. Every behavior with level "must" must appear in the "covers" of at least one rule/component/endpoint you return, or of the base spec, or in not_carried with a reason. Use behavior ids exactly as given.
63
+ 2. Read the code in <sources> around each behavior (line numbers are given) to understand intent: what triggers it, in which form mode it runs (VR.isNew / VR.isEdit / which page loads the script), and for whom (group checks).
64
+ 3. One legacy behaviour often spans several inventory entries (an event binding, a toggle, a PreSaveAction check). Prefer one rule that covers all of them.
65
+ 4. Express mode-specific logic with `modes`; express group-specific logic with {"role": ...} conditions.
66
+ 5. If something is ambiguous, still choose the most literal reading and add a short question to open_questions.
67
+ </how_to_work>
68
+
69
+ <output_contract>
70
+ Respond with one JSON object and nothing else:
71
+ {
72
+ "rules": [ ...new rules (ids R1, R2, ...) ],
73
+ "components": [ ...components to add or replace (same id replaces a base component) ],
74
+ "endpoints": [ ...endpoints to add or replace (same id replaces) ],
75
+ "not_carried": [ {"covers": [...], "reason": "..."} ],
76
+ "open_questions": [ "..." ]
77
+ }
78
+ Do not repeat the base spec's schema rules (ids S1, S2, ...) or fields. If you receive <validation_feedback>, fix every listed problem and return the complete JSON object again.
79
+ </output_contract>