@rubytech/create-maxy-code 0.1.485 → 0.1.487
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/package.json +1 -1
- package/payload/platform/docs/superpowers/plans/2026-07-21-graph-caption-resolver.md +124 -0
- package/payload/platform/docs/superpowers/plans/2026-07-21-task-1877-bash-schema-enforcement.md +107 -0
- package/payload/platform/docs/superpowers/plans/2026-07-21-task-1887-reconcile-allowed-top-level.md +205 -0
- package/payload/platform/lib/graph-style/dist/index.d.ts +4 -1
- package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-style/dist/index.js +109 -1
- package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-style/src/__tests__/caption-coverage.test.ts +117 -0
- package/payload/platform/lib/graph-style/src/index.ts +81 -1
- package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh +159 -0
- package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh +18 -6
- package/payload/platform/plugins/admin/hooks/fs-schema-guard-bash-post.sh +141 -0
- package/payload/platform/plugins/admin/hooks/fs-schema-guard-bash-pre.sh +66 -0
- package/payload/platform/plugins/admin/hooks/fs-schema-guard.sh +33 -1
- package/payload/platform/plugins/admin/skills/professional-document/SKILL.md +3 -3
- package/payload/platform/plugins/cloudflare/PLUGIN.md +5 -3
- package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +23 -2
- package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +30 -8
- package/payload/platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs +22 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +2 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +49 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +57 -3
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +23 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +44 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-session-grant.test.ts +67 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/schema-exposed-dirs.test.ts +43 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +54 -5
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +10 -1
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +19 -5
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +19 -2
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +8 -2
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/index.html +28 -5
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.css +195 -6
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +193 -10
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +4 -3
- package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +5 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-select.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-select.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-select.test.js +50 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-select.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-text.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-text.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-text.test.js +51 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-text.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-fetch-attachment.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-fetch-attachment.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-fetch-attachment.test.js +89 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-fetch-attachment.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/index.js +28 -0
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-text.d.ts +22 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-text.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-text.js +55 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-text.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +32 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js +75 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch-attachment.d.ts +19 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch-attachment.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch-attachment.js +49 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch-attachment.js.map +1 -0
- package/payload/platform/plugins/email/references/email-reference.md +7 -1
- package/payload/platform/plugins/memory/references/schema-construction.md +18 -12
- package/payload/platform/plugins/memory/references/schema-estate-agent.md +11 -6
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.js +12 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.d.ts +8 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.js +12 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js +12 -2
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js.map +1 -1
- package/payload/platform/scripts/__tests__/account-schema-owned-dirs.test.sh +75 -6
- package/payload/platform/scripts/lib/account-schema-owned-dirs.py +111 -22
- package/payload/platform/scripts/lib/provision-account-dir.sh +8 -1
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts +39 -0
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +315 -0
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +15 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js +4 -0
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
- package/payload/platform/templates/account-schema/SCHEMA.md +17 -0
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/server/{chunk-XGNRSM57.js → chunk-HN4HFTXE.js} +6 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/{admin-rScqRq-R.js → admin-BoVCP4Mw.js} +1 -1
- package/payload/server/public/assets/{data-TOWuhMFb.js → data-CtOxa94P.js} +1 -1
- package/payload/server/public/assets/{graph-labels-Axa04w8A.js → graph-labels-C8-sIKuv.js} +1 -1
- package/payload/server/public/assets/{graph-BEGs_P2m.js → graph-mJ9ky5dc.js} +1 -1
- package/payload/server/public/assets/{page-6CZ8R57f.js → page-Cv0qUwMG.js} +1 -1
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +2 -2
- package/payload/server/public/index.html +3 -3
- package/payload/server/server.js +231 -103
|
@@ -110,31 +110,46 @@ merge_owned_dirs_into_schema "$A_TB" >/dev/null
|
|
|
110
110
|
ALLOWED_TB="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_TB/SCHEMA.md")"
|
|
111
111
|
assert_grep "ledger" "$ALLOWED_TB" "shapeb-allowlist-has-ledger"
|
|
112
112
|
|
|
113
|
-
# --- construction vertical:
|
|
113
|
+
# --- construction vertical: root buckets derived, job-owned entities nested ---
|
|
114
114
|
T_C="$(make_vertical_tree '["sitedesk"]' 'schema-construction')"; A_C="$(make_account "$T_C")"
|
|
115
115
|
export PROJECT_DIR="$T_C/platform"
|
|
116
116
|
OUT_C="$(merge_owned_dirs_into_schema "$A_C")"
|
|
117
117
|
# Match a non-empty derived list, not the bare key (which prints even for zero).
|
|
118
118
|
assert_grep "domainBuckets=jobs" "$OUT_C" "con-domain-log"
|
|
119
|
+
# Summary reports the full root set and the job-owned entities with their parent.
|
|
120
|
+
assert_grep "roots=jobs,customers,customer-sites,assets,parts,ppm-contracts" "$OUT_C" "con-roots-log"
|
|
121
|
+
assert_grep "nested=Visit<-Job,Quote<-Job,PurchaseOrder<-Job" "$OUT_C" "con-nested-log"
|
|
119
122
|
ALLOWED_C="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_C/SCHEMA.md")"
|
|
120
|
-
|
|
123
|
+
# Root entities (blank Owner) get a top-level bucket.
|
|
124
|
+
for d in jobs customers customer-sites assets parts ppm-contracts; do
|
|
121
125
|
assert_grep "$d" "$ALLOWED_C" "con-allow-$d"
|
|
122
126
|
done
|
|
127
|
+
# Job-owned entities (Owner=Job) do NOT get a root bucket.
|
|
128
|
+
for d in visits quotes purchase-orders; do
|
|
129
|
+
assert_nogrep "$d" "$ALLOWED_C" "con-noroot-$d"
|
|
130
|
+
done
|
|
123
131
|
assert_nogrep "whatsapp-groups" "$ALLOWED_C" "con-no-transport-bucket"
|
|
124
132
|
assert_grep "customer-sites" "$ALLOWED_C" "con-site-renamed"
|
|
125
|
-
|
|
133
|
+
SCHEMA_C="$(cat "$A_C/SCHEMA.md")"
|
|
134
|
+
assert_grep "Domain entity buckets" "$SCHEMA_C" "con-region"
|
|
135
|
+
# The descriptive region tells the agent where a job-owned entity's records live.
|
|
136
|
+
assert_grep '`Quote` records live under `jobs/` (owned by `Job`), not a root bucket.' "$SCHEMA_C" "con-nested-line"
|
|
126
137
|
B_C="$(shasum "$A_C/SCHEMA.md" | awk '{print $1}')"
|
|
127
138
|
merge_owned_dirs_into_schema "$A_C" >/dev/null
|
|
128
139
|
assert_eq "$(shasum "$A_C/SCHEMA.md" | awk '{print $1}')" "$B_C" "con-idempotent"
|
|
129
140
|
|
|
130
|
-
# --- estate-agent vertical:
|
|
141
|
+
# --- estate-agent vertical: root buckets only, listing-owned entities nested ---
|
|
131
142
|
T_E="$(make_vertical_tree '["real-agent"]' 'schema-estate-agent')"; A_E="$(make_account "$T_E")"
|
|
132
143
|
export PROJECT_DIR="$T_E/platform"
|
|
133
144
|
merge_owned_dirs_into_schema "$A_E" >/dev/null
|
|
134
145
|
ALLOWED_E="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_E/SCHEMA.md")"
|
|
135
|
-
|
|
146
|
+
# Root entities get a bucket; Listing-owned Viewing/Offer do not.
|
|
147
|
+
for d in properties listings; do
|
|
136
148
|
assert_grep "$d" "$ALLOWED_E" "ea-allow-$d"
|
|
137
149
|
done
|
|
150
|
+
for d in viewings offers; do
|
|
151
|
+
assert_nogrep "$d" "$ALLOWED_E" "ea-noroot-$d"
|
|
152
|
+
done
|
|
138
153
|
assert_nogrep "jobs" "$ALLOWED_E" "ea-no-sitedesk-jobs"
|
|
139
154
|
|
|
140
155
|
# --- no vertical declared: generic base only ---
|
|
@@ -145,14 +160,68 @@ ALLOWED_N="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_N/SCHEM
|
|
|
145
160
|
assert_nogrep "jobs" "$ALLOWED_N" "novert-no-domain"
|
|
146
161
|
assert_nogrep "Domain entity buckets" "$(cat "$A_N/SCHEMA.md")" "novert-no-region"
|
|
147
162
|
|
|
148
|
-
# --- reconcile
|
|
163
|
+
# --- reconcile: root entity present/absent, job-owned entity never at root ---
|
|
149
164
|
A_RCD="$(make_account "$T_C")"
|
|
150
165
|
export PROJECT_DIR="$T_C/platform"
|
|
151
166
|
RCD_BEFORE="$(PROJECT_DIR="$PROJECT_DIR" python3 "$PLAT_SCRIPTS/lib/account-schema-owned-dirs.py" reconcile "$A_RCD")"
|
|
152
167
|
assert_grep "ontologyEntity=Job bucket=jobs present=false" "$RCD_BEFORE" "rcd-job-absent"
|
|
168
|
+
# A job-owned entity is reported as not-expected-at-root, not as a plain bucket.
|
|
169
|
+
assert_grep "ontologyEntity=Quote owner=Job bucket=quotes expectedAtRoot=false stray=false" "$RCD_BEFORE" "rcd-quote-nested"
|
|
153
170
|
merge_owned_dirs_into_schema "$A_RCD" >/dev/null
|
|
154
171
|
RCD_AFTER="$(PROJECT_DIR="$PROJECT_DIR" python3 "$PLAT_SCRIPTS/lib/account-schema-owned-dirs.py" reconcile "$A_RCD")"
|
|
155
172
|
assert_grep "ontologyEntity=Job bucket=jobs present=true" "$RCD_AFTER" "rcd-job-present"
|
|
173
|
+
# After a clean merge the job-owned bucket is still absent from root (no stray).
|
|
174
|
+
assert_grep "ontologyEntity=Quote owner=Job bucket=quotes expectedAtRoot=false stray=false" "$RCD_AFTER" "rcd-quote-no-stray"
|
|
175
|
+
|
|
176
|
+
# --- reconcile: a pre-fix account still listing a job-owned bucket at root is a stray ---
|
|
177
|
+
A_STRAY="$(make_account "$T_C")"
|
|
178
|
+
export PROJECT_DIR="$T_C/platform"
|
|
179
|
+
# Seed the old flat-rule layout: inject `quotes` into the allowed-top-level fence.
|
|
180
|
+
awk '/^projects$/{print; print "quotes"; next} {print}' "$A_STRAY/SCHEMA.md" > "$A_STRAY/SCHEMA.md.tmp" && mv "$A_STRAY/SCHEMA.md.tmp" "$A_STRAY/SCHEMA.md"
|
|
181
|
+
RC_STRAY="$(PROJECT_DIR="$PROJECT_DIR" python3 "$PLAT_SCRIPTS/lib/account-schema-owned-dirs.py" reconcile "$A_STRAY")"
|
|
182
|
+
assert_grep "ontologyEntity=Quote owner=Job bucket=quotes expectedAtRoot=false stray=true" "$RC_STRAY" "rcd-quote-stray"
|
|
183
|
+
|
|
184
|
+
# insert_stray <schema_path> <name>: add <name> as the first entry inside the
|
|
185
|
+
# allowed-top-level fence (simulates an account whose list carries a bucket no
|
|
186
|
+
# longer in the ontology).
|
|
187
|
+
insert_stray() {
|
|
188
|
+
awk -v n="$2" '{print} /^```allowed-top-level$/{print n}' "$1" > "$1.tmp" && mv "$1.tmp" "$1"
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# --- prune: stale bucket, absent on disk -> removed from list + logged ---
|
|
192
|
+
T_PRUNE="$(make_vertical_tree '["sitedesk"]' 'schema-construction')"
|
|
193
|
+
A_P="$(make_account "$T_PRUNE")"; export PROJECT_DIR="$T_PRUNE/platform"
|
|
194
|
+
insert_stray "$A_P/SCHEMA.md" "inbound-invoices"
|
|
195
|
+
OUT_P="$(merge_owned_dirs_into_schema "$A_P")"
|
|
196
|
+
ALLOWED_P="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_P/SCHEMA.md")"
|
|
197
|
+
assert_nogrep "inbound-invoices" "$ALLOWED_P" "prune-empty-removed-from-list"
|
|
198
|
+
assert_grep "removed=inbound-invoices" "$OUT_P" "prune-empty-logged"
|
|
199
|
+
|
|
200
|
+
# --- keep: stale bucket that holds files -> retained + keptPopulated ---
|
|
201
|
+
T_KEEP="$(make_vertical_tree '["sitedesk"]' 'schema-construction')"
|
|
202
|
+
A_K="$(make_account "$T_KEEP")"; export PROJECT_DIR="$T_KEEP/platform"
|
|
203
|
+
insert_stray "$A_K/SCHEMA.md" "inbound-invoices"
|
|
204
|
+
mkdir -p "$A_K/inbound-invoices"; echo "stamp" > "$A_K/inbound-invoices/stamp_invoice.py"
|
|
205
|
+
OUT_K="$(merge_owned_dirs_into_schema "$A_K")"
|
|
206
|
+
ALLOWED_K="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_K/SCHEMA.md")"
|
|
207
|
+
assert_grep "inbound-invoices" "$ALLOWED_K" "keep-populated-retained"
|
|
208
|
+
assert_grep "keptPopulated=inbound-invoices" "$OUT_K" "keep-populated-logged"
|
|
209
|
+
assert_nogrep "removed=inbound-invoices" "$OUT_K" "keep-populated-not-in-removed"
|
|
210
|
+
|
|
211
|
+
# --- fixed-seed survival: empty base dir is never pruned ---
|
|
212
|
+
T_SEED="$(make_tree '[]')"; A_S="$(make_account "$T_SEED")"; export PROJECT_DIR="$T_SEED/platform"
|
|
213
|
+
OUT_S="$(merge_owned_dirs_into_schema "$A_S")"
|
|
214
|
+
ALLOWED_S="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_S/SCHEMA.md")"
|
|
215
|
+
for d in documents url-get .git output; do
|
|
216
|
+
assert_grep "$d" "$ALLOWED_S" "seed-survives-$d"
|
|
217
|
+
done
|
|
218
|
+
assert_grep "removed=none" "$OUT_S" "seed-nothing-removed"
|
|
219
|
+
|
|
220
|
+
# --- idempotency after a prune: second run byte-identical (same brand) ---
|
|
221
|
+
export PROJECT_DIR="$T_PRUNE/platform"
|
|
222
|
+
B_P="$(shasum "$A_P/SCHEMA.md" | awk '{print $1}')"
|
|
223
|
+
merge_owned_dirs_into_schema "$A_P" >/dev/null
|
|
224
|
+
assert_eq "$(shasum "$A_P/SCHEMA.md" | awk '{print $1}')" "$B_P" "prune-idempotent"
|
|
156
225
|
|
|
157
226
|
echo "PASS=$PASS FAIL=$FAIL"
|
|
158
227
|
if [ "$FAIL" -ne 0 ]; then printf '%s\n' "${FAILED[@]}"; exit 1; fi
|
|
@@ -24,9 +24,35 @@ MARK_END = "<!-- plugin-owned-dirs:end -->"
|
|
|
24
24
|
ONT_START = "<!-- ontology-buckets:start -->"
|
|
25
25
|
ONT_END = "<!-- ontology-buckets:end -->"
|
|
26
26
|
|
|
27
|
+
# Base top-level names shipped in the account template plus `.quarantine`. This
|
|
28
|
+
# set is prune-protection only: a reconcile never *adds* a missing seed entry,
|
|
29
|
+
# it only shields a listed base dir from being flagged stale. Source (verbatim):
|
|
30
|
+
# docs/superpowers/plans/2026-06-23-account-filesystem-schema.md allowed-top-level.
|
|
31
|
+
FIXED_SEED = frozenset({
|
|
32
|
+
"projects", "contacts", "documents", "url-get", "output", "generated",
|
|
33
|
+
"extracted", "uploads", "agents", "specialists", "sites", "public",
|
|
34
|
+
"cache", "secrets", "state", "logs", "tmp", "SCHEMA.md", "account.json",
|
|
35
|
+
"AGENTS.md", ".claude", ".git", ".quarantine",
|
|
36
|
+
})
|
|
37
|
+
|
|
27
38
|
LABEL_DIR_OVERRIDES = {"Site": "customer-sites"}
|
|
28
39
|
|
|
29
40
|
|
|
41
|
+
def _dir_holds_files(account_dir, name):
|
|
42
|
+
"""True iff account_dir/<name> carries file content: a non-empty regular
|
|
43
|
+
file, or a directory whose subtree contains at least one regular file.
|
|
44
|
+
Absent path, empty dir, or a dir tree with no files -> False (prunable)."""
|
|
45
|
+
path = os.path.join(account_dir, name)
|
|
46
|
+
if not os.path.exists(path):
|
|
47
|
+
return False
|
|
48
|
+
if os.path.isfile(path):
|
|
49
|
+
return os.path.getsize(path) > 0
|
|
50
|
+
for _root, _dirs, files in os.walk(path):
|
|
51
|
+
if files:
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
30
56
|
def _label_to_dir(label):
|
|
31
57
|
"""Map a Neo4j label to its file-schema bucket dir: kebab-case-plural,
|
|
32
58
|
lowercased. camelCase word boundaries split on case. One override:
|
|
@@ -147,11 +173,14 @@ def resolve(project_dir):
|
|
|
147
173
|
|
|
148
174
|
|
|
149
175
|
def _top_level_labels(schema_path):
|
|
150
|
-
"""Extract
|
|
176
|
+
"""Extract (label, owner) rows from the entity table under the
|
|
151
177
|
'## Top-level node types' heading of a vertical schema file. A row is
|
|
152
178
|
included iff its 'Schema.org Type' cell is a schema:-namespaced type
|
|
153
|
-
(excludes transport labels such as WhatsAppGroup -> cdm:Channel).
|
|
154
|
-
|
|
179
|
+
(excludes transport labels such as WhatsAppGroup -> cdm:Channel). `owner`
|
|
180
|
+
is the containment parent from the 'Owner' column: None when the table has
|
|
181
|
+
no Owner column, '' when the column is present but the cell is blank (a root
|
|
182
|
+
entity), else the parent label (a contained entity). Returns rows in table
|
|
183
|
+
order. No such heading -> empty list."""
|
|
155
184
|
try:
|
|
156
185
|
with open(schema_path) as f:
|
|
157
186
|
lines = f.read().split("\n")
|
|
@@ -176,6 +205,8 @@ def _top_level_labels(schema_path):
|
|
|
176
205
|
cols = [c.strip() for c in lines[hdr].strip().strip("|").split("|")]
|
|
177
206
|
label_i = cols.index("Neo4j Label")
|
|
178
207
|
type_i = cols.index("Schema.org Type")
|
|
208
|
+
owner_i = cols.index("Owner") if "Owner" in cols else None
|
|
209
|
+
need = max(label_i, type_i, owner_i if owner_i is not None else 0)
|
|
179
210
|
out = []
|
|
180
211
|
for k in range(hdr + 2, len(lines)): # +2 skips the header separator row
|
|
181
212
|
row = lines[k]
|
|
@@ -184,19 +215,24 @@ def _top_level_labels(schema_path):
|
|
|
184
215
|
if "|" not in row or not row.strip():
|
|
185
216
|
break
|
|
186
217
|
cells = [c.strip() for c in row.strip().strip("|").split("|")]
|
|
187
|
-
if len(cells) <=
|
|
218
|
+
if len(cells) <= need:
|
|
188
219
|
continue
|
|
189
220
|
label = cells[label_i].strip("`").strip()
|
|
190
221
|
stype = cells[type_i].strip("`").strip()
|
|
191
|
-
if label and stype.startswith("schema:"):
|
|
192
|
-
|
|
222
|
+
if not (label and stype.startswith("schema:")):
|
|
223
|
+
continue
|
|
224
|
+
owner = None if owner_i is None else cells[owner_i].strip("`").strip()
|
|
225
|
+
out.append((label, owner))
|
|
193
226
|
return out
|
|
194
227
|
|
|
195
228
|
|
|
196
229
|
def resolve_domain_buckets(project_dir):
|
|
197
230
|
"""Domain operator-entity buckets derived from the brand's vertical
|
|
198
|
-
ontology. Returns [{dir, label}] in table order, deduped by dir.
|
|
199
|
-
|
|
231
|
+
ontology. Returns [{dir, label, owner}] in table order, deduped by dir.
|
|
232
|
+
`owner` is None (the vertical table has no Owner column), '' (a root entity
|
|
233
|
+
that owns a top-level bucket), or the parent label (a contained entity that
|
|
234
|
+
gets no root bucket, its records living under the parent's folder). Empty
|
|
235
|
+
when the brand declares no vertical or the vertical file has no top-level
|
|
200
236
|
section."""
|
|
201
237
|
vertical = _read_vertical(project_dir)
|
|
202
238
|
if not vertical:
|
|
@@ -204,15 +240,22 @@ def resolve_domain_buckets(project_dir):
|
|
|
204
240
|
schema_path = os.path.join(project_dir, "plugins", "memory", "references", vertical + ".md")
|
|
205
241
|
seen = set()
|
|
206
242
|
out = []
|
|
207
|
-
for label in _top_level_labels(schema_path):
|
|
243
|
+
for label, owner in _top_level_labels(schema_path):
|
|
208
244
|
d = _label_to_dir(label)
|
|
209
245
|
if d in seen:
|
|
210
246
|
continue
|
|
211
247
|
seen.add(d)
|
|
212
|
-
out.append({"dir": d, "label": label})
|
|
248
|
+
out.append({"dir": d, "label": label, "owner": owner})
|
|
213
249
|
return out
|
|
214
250
|
|
|
215
251
|
|
|
252
|
+
def _is_root(owner):
|
|
253
|
+
"""A domain bucket is a root (own top-level dir) when its Owner is blank
|
|
254
|
+
('') or the vertical declares no Owner column at all (None). A non-empty
|
|
255
|
+
owner names the containing parent, so the entity nests and emits no root."""
|
|
256
|
+
return owner is None or owner == ""
|
|
257
|
+
|
|
258
|
+
|
|
216
259
|
def _allowed_block(text):
|
|
217
260
|
"""Return (list_of_entries, start_idx, end_idx) for the fenced
|
|
218
261
|
allowed-top-level block. end_idx is the closing-fence line index."""
|
|
@@ -246,28 +289,49 @@ def merge(project_dir, account_dir):
|
|
|
246
289
|
domain = resolve_domain_buckets(project_dir)
|
|
247
290
|
schema_path = os.path.join(account_dir, "SCHEMA.md")
|
|
248
291
|
if not os.path.isfile(schema_path):
|
|
249
|
-
print(f"brand={brand} added=none domainBuckets=none")
|
|
292
|
+
print(f"brand={brand} added=none domainBuckets=none removed=none keptPopulated=none")
|
|
250
293
|
return
|
|
251
294
|
with open(schema_path) as f:
|
|
252
295
|
text = f.read()
|
|
253
296
|
entries, start, end = _allowed_block(text)
|
|
254
297
|
added = []
|
|
255
298
|
added_domain = []
|
|
299
|
+
removed = []
|
|
300
|
+
kept_populated = []
|
|
256
301
|
if entries is not None:
|
|
257
302
|
present = set(entries)
|
|
258
|
-
|
|
303
|
+
# Only ROOT domain buckets belong at the account root; a contained
|
|
304
|
+
# entity (non-blank Owner) nests under its owner and is not a target,
|
|
305
|
+
# so a pre-fix account's empty owned bucket prunes while a populated one
|
|
306
|
+
# is kept-and-logged like any other stale bucket.
|
|
307
|
+
target = FIXED_SEED | {e["dir"] for e in owned} | {b["dir"] for b in domain if _is_root(b["owner"])}
|
|
308
|
+
# Reconcile existing entries: keep in-target; keep populated strays
|
|
309
|
+
# (logged); drop empty/absent strays (logged).
|
|
310
|
+
new_entries = []
|
|
311
|
+
for entry in entries:
|
|
312
|
+
if entry in target:
|
|
313
|
+
new_entries.append(entry)
|
|
314
|
+
elif _dir_holds_files(account_dir, entry):
|
|
315
|
+
new_entries.append(entry)
|
|
316
|
+
kept_populated.append(entry)
|
|
317
|
+
else:
|
|
318
|
+
removed.append(entry)
|
|
319
|
+
# Append newly-projected owned then domain dirs not already present.
|
|
259
320
|
insert = []
|
|
260
321
|
for e in owned:
|
|
261
322
|
if e["dir"] not in present and e["dir"] not in insert:
|
|
262
323
|
insert.append(e["dir"])
|
|
263
324
|
added.append(e["dir"])
|
|
264
325
|
for b in domain:
|
|
326
|
+
if not _is_root(b["owner"]):
|
|
327
|
+
continue # contained entity: no root bucket, nests under its owner
|
|
265
328
|
if b["dir"] not in present and b["dir"] not in insert:
|
|
266
329
|
insert.append(b["dir"])
|
|
267
330
|
added_domain.append(b["dir"])
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
331
|
+
new_entries.extend(insert)
|
|
332
|
+
lines = text.split("\n")
|
|
333
|
+
lines[start + 1:end] = new_entries
|
|
334
|
+
text = "\n".join(lines)
|
|
271
335
|
# Regenerate both descriptive regions wholesale (idempotent).
|
|
272
336
|
text = _strip_region(text, MARK_START, MARK_END)
|
|
273
337
|
text = _strip_region(text, ONT_START, ONT_END)
|
|
@@ -284,20 +348,33 @@ def merge(project_dir, account_dir):
|
|
|
284
348
|
owned_dirs = {e["dir"] for e in owned}
|
|
285
349
|
# A dir already claimed by a plugin (described in the plugin-owned region)
|
|
286
350
|
# is not also described here, so a name collision cannot double-describe.
|
|
287
|
-
|
|
288
|
-
if
|
|
351
|
+
described = [b for b in domain if b["dir"] not in owned_dirs]
|
|
352
|
+
root_described = [b for b in described if _is_root(b["owner"])]
|
|
353
|
+
nested_described = [b for b in described if not _is_root(b["owner"])]
|
|
354
|
+
if root_described or nested_described:
|
|
289
355
|
region = [ONT_START, "## Domain entity buckets (from the graph ontology)", "",
|
|
290
356
|
"One bucket per top-level operator-entry node type of this brand's",
|
|
291
357
|
"business ontology. File a job's artifacts under its entity folder;",
|
|
292
|
-
"the folder may hold a subtree.",
|
|
293
|
-
|
|
358
|
+
"the folder may hold a subtree. A contained entity has no root",
|
|
359
|
+
"bucket; its records live under the owning entity's folder.", ""]
|
|
360
|
+
for b in root_described:
|
|
294
361
|
region.append(f"- `{b['dir']}/` - one folder per {b['label']} record.")
|
|
362
|
+
for b in nested_described:
|
|
363
|
+
parent_dir = _label_to_dir(b["owner"])
|
|
364
|
+
region.append(f"- `{b['label']}` records live under `{parent_dir}/` "
|
|
365
|
+
f"(owned by `{b['owner']}`), not a root bucket.")
|
|
295
366
|
region.append(ONT_END)
|
|
296
367
|
text = text.rstrip("\n") + "\n\n" + "\n".join(region) + "\n"
|
|
297
368
|
with open(schema_path, "w") as f:
|
|
298
369
|
f.write(text)
|
|
370
|
+
root_dirs = [b["dir"] for b in domain if _is_root(b["owner"])]
|
|
371
|
+
nested_pairs = [f"{b['label']}<-{b['owner']}" for b in domain if not _is_root(b["owner"])]
|
|
299
372
|
print(f"brand={brand} added={','.join(added) if added else 'none'} "
|
|
300
|
-
f"domainBuckets={','.join(added_domain) if added_domain else 'none'}"
|
|
373
|
+
f"domainBuckets={','.join(added_domain) if added_domain else 'none'} "
|
|
374
|
+
f"removed={','.join(removed) if removed else 'none'} "
|
|
375
|
+
f"keptPopulated={','.join(kept_populated) if kept_populated else 'none'} "
|
|
376
|
+
f"roots={','.join(root_dirs) if root_dirs else 'none'} "
|
|
377
|
+
f"nested={','.join(nested_pairs) if nested_pairs else 'none'}")
|
|
301
378
|
|
|
302
379
|
|
|
303
380
|
def reconcile(project_dir, account_dir):
|
|
@@ -316,9 +393,21 @@ def reconcile(project_dir, account_dir):
|
|
|
316
393
|
domain = resolve_domain_buckets(project_dir)
|
|
317
394
|
if vertical and not domain:
|
|
318
395
|
print(f"brand={brand} ontologyVertical={vertical} topLevelSection=absent")
|
|
396
|
+
# A vertical whose top-level table carries no Owner column cannot state
|
|
397
|
+
# containment; every entity falls back to root, but loudly, never silently.
|
|
398
|
+
if vertical and any(b["owner"] is None for b in domain):
|
|
399
|
+
print(f"brand={brand} ontologyVertical={vertical} owner-source=absent")
|
|
319
400
|
for b in domain:
|
|
320
|
-
|
|
321
|
-
|
|
401
|
+
if _is_root(b["owner"]):
|
|
402
|
+
is_present = "true" if b["dir"] in present else "false"
|
|
403
|
+
print(f"brand={brand} ontologyEntity={b['label']} bucket={b['dir']} present={is_present}")
|
|
404
|
+
else:
|
|
405
|
+
# Contained entity: no root bucket expected. A bucket still listed at
|
|
406
|
+
# root is a pre-fix stray the standing fs-reconcile quarantines once
|
|
407
|
+
# the owned bucket leaves the allowed set (unless it holds files).
|
|
408
|
+
is_stray = "true" if b["dir"] in present else "false"
|
|
409
|
+
print(f"brand={brand} ontologyEntity={b['label']} owner={b['owner']} "
|
|
410
|
+
f"bucket={b['dir']} expectedAtRoot=false stray={is_stray}")
|
|
322
411
|
|
|
323
412
|
|
|
324
413
|
def main():
|
|
@@ -86,7 +86,8 @@ provision_account_dir() {
|
|
|
86
86
|
{
|
|
87
87
|
"matcher": "Bash",
|
|
88
88
|
"hooks": [
|
|
89
|
-
{ "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
|
|
89
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" },
|
|
90
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/fs-schema-guard-bash-pre.sh" }
|
|
90
91
|
]
|
|
91
92
|
},
|
|
92
93
|
{
|
|
@@ -115,6 +116,12 @@ provision_account_dir() {
|
|
|
115
116
|
}
|
|
116
117
|
],
|
|
117
118
|
"PostToolUse": [
|
|
119
|
+
{
|
|
120
|
+
"matcher": "Bash",
|
|
121
|
+
"hooks": [
|
|
122
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/fs-schema-guard-bash-post.sh" }
|
|
123
|
+
]
|
|
124
|
+
},
|
|
118
125
|
{
|
|
119
126
|
"matcher": "Agent",
|
|
120
127
|
"hooks": [
|
package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface ReconcileCounts {
|
|
2
|
+
strayTopLevel: number;
|
|
3
|
+
overDeep: number;
|
|
4
|
+
quarantined: number;
|
|
5
|
+
badName: number;
|
|
6
|
+
}
|
|
7
|
+
/** The single per-run summary line body. A non-zero count is the standing signal
|
|
8
|
+
* that an agent authored folders the hard guards could not prevent. Pure
|
|
9
|
+
* formatter so the emitter and its test share one wording. */
|
|
10
|
+
export declare function formatReconcileLine(c: ReconcileCounts): string;
|
|
11
|
+
/** Parse the fenced ```allowed-top-level block from an account's SCHEMA.md —
|
|
12
|
+
* the same structured set fs-schema-guard.sh parses with awk. Returns the
|
|
13
|
+
* entry names (blank lines dropped). An absent block yields an empty set. */
|
|
14
|
+
export declare function parseAllowedTopLevel(schemaText: string): Set<string>;
|
|
15
|
+
/** Reconcile one account dir. Reads its SCHEMA.md allowed set; an absent/empty
|
|
16
|
+
* set skips the account (fail open, like the hook — an un-seeded legacy account
|
|
17
|
+
* must not have its whole root quarantined). Moves stray top-level entries to
|
|
18
|
+
* .quarantine/ with a manifest line; counts over-deep files without moving. */
|
|
19
|
+
export declare function reconcileAccount(accountDir: string, now: () => number): ReconcileCounts;
|
|
20
|
+
export interface ReconcileDeps {
|
|
21
|
+
accountsRoot: string;
|
|
22
|
+
logger: (line: string) => void;
|
|
23
|
+
now?: () => number;
|
|
24
|
+
}
|
|
25
|
+
/** One reconcile pass over every account under the accounts root, then emit the
|
|
26
|
+
* single summary line. An account is any child dir with an account.json (the
|
|
27
|
+
* account marker the census uses). Per-account errors are swallowed so one bad
|
|
28
|
+
* account never aborts the run. */
|
|
29
|
+
export declare function runReconcile(deps: ReconcileDeps): void;
|
|
30
|
+
export interface AccountDirSchemaReconcile {
|
|
31
|
+
start(): void;
|
|
32
|
+
stop(): void;
|
|
33
|
+
}
|
|
34
|
+
/** Standing periodic reconcile. Unref'd interval so it never holds the process
|
|
35
|
+
* open; each tick re-runs the pass best-effort. */
|
|
36
|
+
export declare function createAccountDirSchemaReconcile(deps: ReconcileDeps & {
|
|
37
|
+
intervalMs: number;
|
|
38
|
+
}): AccountDirSchemaReconcile;
|
|
39
|
+
//# sourceMappingURL=account-dir-schema-reconcile.d.ts.map
|
package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-dir-schema-reconcile.d.ts","sourceRoot":"","sources":["../src/account-dir-schema-reconcile.ts"],"names":[],"mappings":"AA2FA,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;+DAE+D;AAC/D,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,eAAe,GAAG,MAAM,CAE9D;AAED;;8EAE8E;AAC9E,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAcpE;AA0FD;;;gFAGgF;AAChF,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,MAAM,GAAG,eAAe,CAwDvF;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED;;;oCAGoC;AACpC,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAwBtD;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,IAAI,IAAI,CAAA;IACb,IAAI,IAAI,IAAI,CAAA;CACb;AAED;oDACoD;AACpD,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,aAAa,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC3C,yBAAyB,CAiB3B"}
|