wicked-crew 0.7.28 → 0.7.30
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/README.md +12 -1
- package/dist/api/chat-scope.d.ts +210 -0
- package/dist/api/chat-scope.d.ts.map +1 -0
- package/dist/api/chat-scope.js +728 -0
- package/dist/api/chat-scope.js.map +1 -0
- package/dist/api/diagnostics.d.ts +30 -2
- package/dist/api/diagnostics.d.ts.map +1 -1
- package/dist/api/diagnostics.js +73 -5
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/api/governance-health.d.ts +171 -0
- package/dist/api/governance-health.d.ts.map +1 -0
- package/dist/api/governance-health.js +379 -0
- package/dist/api/governance-health.js.map +1 -0
- package/dist/api/open-path.d.ts.map +1 -1
- package/dist/api/open-path.js +2 -1
- package/dist/api/open-path.js.map +1 -1
- package/dist/api/post-hoc-deliver.d.ts +9 -5
- package/dist/api/post-hoc-deliver.d.ts.map +1 -1
- package/dist/api/post-hoc-deliver.js +21 -7
- package/dist/api/post-hoc-deliver.js.map +1 -1
- package/dist/api/routes.d.ts +70 -12
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +375 -60
- package/dist/api/routes.js.map +1 -1
- package/dist/api/seat-health.d.ts +38 -0
- package/dist/api/seat-health.d.ts.map +1 -1
- package/dist/api/seat-health.js +88 -5
- package/dist/api/seat-health.js.map +1 -1
- package/dist/api/seat-signin.d.ts.map +1 -1
- package/dist/api/seat-signin.js +49 -23
- package/dist/api/seat-signin.js.map +1 -1
- package/dist/api/seat-standing.d.ts +101 -0
- package/dist/api/seat-standing.d.ts.map +1 -0
- package/dist/api/seat-standing.js +138 -0
- package/dist/api/seat-standing.js.map +1 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +37 -0
- package/dist/api/server.js.map +1 -1
- package/dist/campaigns/routes.d.ts +4 -4
- package/dist/cli/governance.d.ts +131 -0
- package/dist/cli/governance.d.ts.map +1 -0
- package/dist/cli/governance.js +354 -0
- package/dist/cli/governance.js.map +1 -0
- package/dist/cli/index.js +102 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/core/adapter.d.ts +99 -1
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +149 -8
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/bridge-path.d.ts +21 -0
- package/dist/core/bridge-path.d.ts.map +1 -1
- package/dist/core/bridge-path.js +37 -0
- package/dist/core/bridge-path.js.map +1 -1
- package/dist/core/bridge-reaper.d.ts.map +1 -1
- package/dist/core/bridge-reaper.js +11 -4
- package/dist/core/bridge-reaper.js.map +1 -1
- package/dist/core/deliver-text.d.ts +184 -0
- package/dist/core/deliver-text.d.ts.map +1 -0
- package/dist/core/deliver-text.js +434 -0
- package/dist/core/deliver-text.js.map +1 -0
- package/dist/core/deliver-triage.d.ts +90 -0
- package/dist/core/deliver-triage.d.ts.map +1 -0
- package/dist/core/deliver-triage.js +106 -0
- package/dist/core/deliver-triage.js.map +1 -0
- package/dist/core/deliver.d.ts +103 -9
- package/dist/core/deliver.d.ts.map +1 -1
- package/dist/core/deliver.js +244 -46
- package/dist/core/deliver.js.map +1 -1
- package/dist/core/estate-mcp-client.d.ts.map +1 -1
- package/dist/core/estate-mcp-client.js +7 -2
- package/dist/core/estate-mcp-client.js.map +1 -1
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +11 -1
- package/dist/core/exec.js.map +1 -1
- package/dist/core/governance-store.d.ts +219 -0
- package/dist/core/governance-store.d.ts.map +1 -0
- package/dist/core/governance-store.js +290 -0
- package/dist/core/governance-store.js.map +1 -0
- package/dist/core/repoPaths.d.ts +36 -5
- package/dist/core/repoPaths.d.ts.map +1 -1
- package/dist/core/repoPaths.js +46 -4
- package/dist/core/repoPaths.js.map +1 -1
- package/dist/interactive/bridge-pool.d.ts +29 -24
- package/dist/interactive/bridge-pool.d.ts.map +1 -1
- package/dist/interactive/bridge-pool.js +74 -7
- package/dist/interactive/bridge-pool.js.map +1 -1
- package/dist/interactive/chat-events.d.ts.map +1 -1
- package/dist/interactive/chat-events.js +5 -2
- package/dist/interactive/chat-events.js.map +1 -1
- package/dist/interactive/council-outcome.d.ts +28 -0
- package/dist/interactive/council-outcome.d.ts.map +1 -0
- package/dist/interactive/council-outcome.js +57 -0
- package/dist/interactive/council-outcome.js.map +1 -0
- package/dist/interactive/demo-events.d.ts.map +1 -1
- package/dist/interactive/demo-events.js +5 -2
- package/dist/interactive/demo-events.js.map +1 -1
- package/dist/interactive/doc-delete-routes.d.ts.map +1 -1
- package/dist/interactive/doc-delete-routes.js +3 -2
- package/dist/interactive/doc-delete-routes.js.map +1 -1
- package/dist/interactive/draft-events.d.ts.map +1 -1
- package/dist/interactive/draft-events.js +5 -2
- package/dist/interactive/draft-events.js.map +1 -1
- package/dist/interactive/edit-events.d.ts.map +1 -1
- package/dist/interactive/edit-events.js +5 -2
- package/dist/interactive/edit-events.js.map +1 -1
- package/dist/projects/auto-refresh.d.ts +54 -0
- package/dist/projects/auto-refresh.d.ts.map +1 -0
- package/dist/projects/auto-refresh.js +76 -0
- package/dist/projects/auto-refresh.js.map +1 -0
- package/dist/projects/graph-paths.d.ts +4 -2
- package/dist/projects/graph-paths.d.ts.map +1 -1
- package/dist/projects/graph-paths.js +4 -2
- package/dist/projects/graph-paths.js.map +1 -1
- package/dist/projects/graph.d.ts +20 -6
- package/dist/projects/graph.d.ts.map +1 -1
- package/dist/projects/graph.js +40 -13
- package/dist/projects/graph.js.map +1 -1
- package/dist/projects/routes.d.ts.map +1 -1
- package/dist/projects/routes.js +30 -11
- package/dist/projects/routes.js.map +1 -1
- package/dist/projects/state-home.d.ts +6 -0
- package/dist/projects/state-home.d.ts.map +1 -1
- package/dist/projects/state-home.js +9 -1
- package/dist/projects/state-home.js.map +1 -1
- package/dist/skills/guards.d.ts +17 -6
- package/dist/skills/guards.d.ts.map +1 -1
- package/dist/skills/guards.js +42 -15
- package/dist/skills/guards.js.map +1 -1
- package/dist/skills/plugin-source.d.ts.map +1 -1
- package/dist/skills/plugin-source.js +3 -0
- package/dist/skills/plugin-source.js.map +1 -1
- package/dist/skills/refs.d.ts +181 -17
- package/dist/skills/refs.d.ts.map +1 -1
- package/dist/skills/refs.js +345 -26
- package/dist/skills/refs.js.map +1 -1
- package/dist/skills/store.d.ts +22 -1
- package/dist/skills/store.d.ts.map +1 -1
- package/dist/skills/store.js +161 -23
- package/dist/skills/store.js.map +1 -1
- package/dist/studio/assets/index-CWUS9Ump.js +542 -0
- package/dist/studio/assets/{index-CK1uJWs2.css → index-_ydd-jKd.css} +1 -1
- package/dist/studio/index.html +2 -2
- package/dist/studio/testid-inventory.json +567 -6
- package/endpoint-manifest.json +67 -14
- package/package.json +4 -4
- package/dist/studio/assets/index-CdmCMh2f.js +0 -539
package/dist/skills/store.js
CHANGED
|
@@ -131,7 +131,7 @@ import { derivedSkillName, parseFrontmatter, PLUGIN_NAME, SKILL_NAME_PREFIX, ski
|
|
|
131
131
|
import { collisionGuard, coreDisableGuard, finding, frontmatterGuard, nameGuard, nestedSkillCreateGuard, noBaselineGuard, nonPortableGuard, SKILL_NAME_RE, supportFileGuard, verdictOf, } from './guards.js';
|
|
132
132
|
import { LiveGenerations } from './live-generations.js';
|
|
133
133
|
import { discoverLivePluginDetailed, gitStateOf, PluginSourceSymlinkError } from './plugin-source.js';
|
|
134
|
-
import { extractPluginRootRefs, extractRelativeRefs, looksBinary,
|
|
134
|
+
import { existsIn, extractPluginRootRefs, extractRelativeRefs, looksBinary, PORTABILITY_EVIDENCE_CAP, PORTABILITY_REASONS, portabilityEvidenceOf, portabilityIssuesOf, portabilityReasonsOf, resolvePluginRootRef, resolveRelativeRef, } from './refs.js';
|
|
135
135
|
import { CURRENT_TMP_PREFIX, STAGING_PREFIX } from './root-names.js';
|
|
136
136
|
import { assertNoSymlinkComponents, copyFiles, EntrySwappedError, hashFileSet, hashTree, impliedDirs, isCarriedFile, makeTreeReadOnly, pruneEmptyDirs, readFileNoFollow, removeTreeForce, sha256Hex, SKIP_DIR_NAMES, SymlinkComponentError, walkEntries, walkFiles, walkTree, writeFileAtomic, } from './tree.js';
|
|
137
137
|
/** The root-level names the store creates live in ONE table (`root-names.ts`, design v3.5 §2) — re-exported for the tests that observe the flip. */
|
|
@@ -302,6 +302,46 @@ export class RevisionMismatchError extends Error {
|
|
|
302
302
|
this.name = 'RevisionMismatchError';
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Why `value` is not a well-formed `SkillPortability` for a row whose `portable` is `portable` —
|
|
307
|
+
* or `null` when it is: an object whose `portable` repeats the row's, whose `reasons` is a
|
|
308
|
+
* strictly sorted, unique list of known tokens (empty exactly when portable), and whose optional
|
|
309
|
+
* `evidence` is at most five strings. Shared by the manifest parse and the snapshot-row check.
|
|
310
|
+
*/
|
|
311
|
+
function portabilityProblem(value, portable) {
|
|
312
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
313
|
+
return 'is not an object';
|
|
314
|
+
const p = value;
|
|
315
|
+
for (const k of Object.keys(p))
|
|
316
|
+
if (!['portable', 'reasons', 'evidence'].includes(k))
|
|
317
|
+
return `carries an unknown key ${JSON.stringify(k)}`;
|
|
318
|
+
if (p['portable'] !== portable)
|
|
319
|
+
return `.portable is ${JSON.stringify(p['portable'])}, not the row's ${String(portable)}`;
|
|
320
|
+
const reasons = p['reasons'];
|
|
321
|
+
if (!Array.isArray(reasons))
|
|
322
|
+
return '.reasons is not an array';
|
|
323
|
+
for (let i = 0; i < reasons.length; i += 1) {
|
|
324
|
+
const r = reasons[i];
|
|
325
|
+
if (typeof r !== 'string' || !PORTABILITY_REASONS.has(r))
|
|
326
|
+
return `.reasons[${i}] is ${JSON.stringify(r)}, not a known reason (${[...PORTABILITY_REASONS].join('|')})`;
|
|
327
|
+
if (i > 0 && !(reasons[i - 1] < r))
|
|
328
|
+
return '.reasons is not sorted and unique';
|
|
329
|
+
}
|
|
330
|
+
if ((reasons.length === 0) !== portable)
|
|
331
|
+
return `.reasons is ${reasons.length === 0 ? 'empty' : 'non-empty'} but the row is ${portable ? 'portable' : 'not portable'}`;
|
|
332
|
+
if (Object.hasOwn(p, 'evidence')) {
|
|
333
|
+
const ev = p['evidence'];
|
|
334
|
+
if (!Array.isArray(ev) || ev.some((e) => typeof e !== 'string'))
|
|
335
|
+
return '.evidence is not an array of strings';
|
|
336
|
+
if (ev.length > PORTABILITY_EVIDENCE_CAP)
|
|
337
|
+
return `.evidence carries ${ev.length} anchors (cap ${PORTABILITY_EVIDENCE_CAP})`;
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
/** Two string lists, equal element for element. */
|
|
342
|
+
function sameStrings(a, b) {
|
|
343
|
+
return a.length === b.length && a.every((x, i) => x === b[i]);
|
|
344
|
+
}
|
|
305
345
|
/**
|
|
306
346
|
* A skill row is `{name, dir}` + a boolean `portable` (the seat-compatibility fact core requires),
|
|
307
347
|
* and — codex round 2: metadata is never trusted to name paths — `name` is a legal skill name,
|
|
@@ -317,6 +357,10 @@ function isSnapshotSkillRow(row) {
|
|
|
317
357
|
return false;
|
|
318
358
|
if (typeof r.dir !== 'string' || typeof r.portable !== 'boolean')
|
|
319
359
|
return false;
|
|
360
|
+
// `portability` (F-079) is optional — older generations lack it — but when present it must be
|
|
361
|
+
// well-formed and agree with `portable`; its reasons are re-derived at verify (`snapshotRowsProblem`).
|
|
362
|
+
if (Object.hasOwn(r, 'portability') && portabilityProblem(r.portability, r.portable) !== null)
|
|
363
|
+
return false;
|
|
320
364
|
// EVERY field is typed (codex round 7): `kind` an enum, `core` / `nested` real booleans — and
|
|
321
365
|
// `nested` IS the fact the dir spells, a row cannot claim otherwise. `kind` and `portable` are
|
|
322
366
|
// re-derived from the generation's own files at verify (`snapshotRowsProblem`); `core` is
|
|
@@ -616,12 +660,12 @@ export class SkillsStore {
|
|
|
616
660
|
throw new SkillsManifestCorruptError(path, `manifest-invalid: ${detail}`);
|
|
617
661
|
};
|
|
618
662
|
const record = (value, where) => typeof value === 'object' && value !== null && !Array.isArray(value) ? value : fail(`${where} is not an object`);
|
|
619
|
-
const exactKeys = (obj, keys, where) => {
|
|
663
|
+
const exactKeys = (obj, keys, where, optional = []) => {
|
|
620
664
|
for (const k of keys)
|
|
621
665
|
if (!(k in obj))
|
|
622
666
|
fail(`${where} lacks ${JSON.stringify(k)}`);
|
|
623
667
|
for (const k of Object.keys(obj))
|
|
624
|
-
if (!keys.includes(k))
|
|
668
|
+
if (!keys.includes(k) && !optional.includes(k))
|
|
625
669
|
fail(`${where} carries an unknown key ${JSON.stringify(k)}`);
|
|
626
670
|
};
|
|
627
671
|
const bool = (value, where) => (typeof value === 'boolean' ? value : fail(`${where} is ${JSON.stringify(value)}, not a boolean`));
|
|
@@ -662,7 +706,9 @@ export class SkillsStore {
|
|
|
662
706
|
}
|
|
663
707
|
const where = `skills[${name}]`;
|
|
664
708
|
const e = record(value, where);
|
|
665
|
-
|
|
709
|
+
// `portability` (F-079) is OPTIONAL: a manifest written before 0.7.30 carries `portable` alone
|
|
710
|
+
// and still loads — the next recompute (any mutation, analyze, publish) fills the field in.
|
|
711
|
+
exactKeys(e, ['dir', 'kind', 'core', 'portable', 'enabled', 'provenance', 'editedAt', 'upgradeAvailable', 'conflict', 'upstreamDir'], where, ['portability']);
|
|
666
712
|
const dir = str(e['dir'], `${where}.dir`);
|
|
667
713
|
const bad = unsafeSkillDir(dir);
|
|
668
714
|
if (bad !== null)
|
|
@@ -670,11 +716,12 @@ export class SkillsStore {
|
|
|
670
716
|
const derived = derivedSkillName(dir.slice(`${SKILLS_SUBDIR}/`.length));
|
|
671
717
|
if (derived !== name)
|
|
672
718
|
fail(`${where} sits at ${dir}, which derives ${JSON.stringify(derived)} — the key must be the path-derived name of its dir`);
|
|
719
|
+
const portable = bool(e['portable'], `${where}.portable`);
|
|
673
720
|
skills[name] = {
|
|
674
721
|
dir,
|
|
675
722
|
kind: oneOf(e['kind'], SKILL_KINDS, `${where}.kind`),
|
|
676
723
|
core: bool(e['core'], `${where}.core`),
|
|
677
|
-
portable
|
|
724
|
+
portable,
|
|
678
725
|
enabled: bool(e['enabled'], `${where}.enabled`),
|
|
679
726
|
provenance: oneOf(e['provenance'], PROVENANCES, `${where}.provenance`),
|
|
680
727
|
editedAt: strOrNull(e['editedAt'], `${where}.editedAt`),
|
|
@@ -682,6 +729,12 @@ export class SkillsStore {
|
|
|
682
729
|
conflict: bool(e['conflict'], `${where}.conflict`),
|
|
683
730
|
upstreamDir: strOrNull(e['upstreamDir'], `${where}.upstreamDir`),
|
|
684
731
|
};
|
|
732
|
+
if (Object.hasOwn(e, 'portability')) {
|
|
733
|
+
const problem = portabilityProblem(e['portability'], portable);
|
|
734
|
+
if (problem !== null)
|
|
735
|
+
fail(`${where}.portability ${problem}`);
|
|
736
|
+
skills[name].portability = e['portability'];
|
|
737
|
+
}
|
|
685
738
|
const upstreamDir = skills[name]?.upstreamDir ?? null;
|
|
686
739
|
if (upstreamDir !== null) {
|
|
687
740
|
const badUpstream = unsafeSkillDir(upstreamDir);
|
|
@@ -876,6 +929,11 @@ export class SkillsStore {
|
|
|
876
929
|
const files = tree.files;
|
|
877
930
|
const byRel = new Map(files.map((f) => [f.rel, f]));
|
|
878
931
|
const dirs = new Set(parsed.skills.map((r) => r.dir));
|
|
932
|
+
// The validator's universe at verify is the generation's OWN bundle files — the views and the
|
|
933
|
+
// metadata file are crew's, not the plugin's — the same universe recompute judged against
|
|
934
|
+
// (closure support files + enabled skills' own files), so the reasons re-derive identically.
|
|
935
|
+
const bundle = new Set(files.map((f) => f.rel).filter((rel) => rel !== SNAPSHOT_MANIFEST_FILENAME && !rel.startsWith(`${VIEWS_DIRNAME}/`)));
|
|
936
|
+
const exists = (p) => existsIn(bundle, p);
|
|
879
937
|
for (const row of parsed.skills) {
|
|
880
938
|
const skillMd = byRel.get(`${row.dir}/SKILL.md`);
|
|
881
939
|
if (skillMd === undefined)
|
|
@@ -886,7 +944,7 @@ export class SkillsStore {
|
|
|
886
944
|
const kind = skillKindOf(fm.fields);
|
|
887
945
|
if (kind !== row.kind)
|
|
888
946
|
return `skill row ${row.name} claims kind ${row.kind}, but its SKILL.md derives ${kind}`;
|
|
889
|
-
|
|
947
|
+
const hits = [];
|
|
890
948
|
const prefix = `${row.dir}/`;
|
|
891
949
|
for (const f of files) {
|
|
892
950
|
if (!f.rel.startsWith(prefix) || owningSkillDir(f.rel, dirs) !== row.dir)
|
|
@@ -894,13 +952,16 @@ export class SkillsStore {
|
|
|
894
952
|
const buf = readFileNoFollow(f.abs);
|
|
895
953
|
if (looksBinary(buf))
|
|
896
954
|
continue;
|
|
897
|
-
|
|
898
|
-
portable = false;
|
|
899
|
-
break;
|
|
900
|
-
}
|
|
955
|
+
hits.push(...portabilityIssuesOf(buf.toString('utf8'), { fileRel: f.rel, skillDir: row.dir, skillDirs: dirs, exists }));
|
|
901
956
|
}
|
|
957
|
+
const reasons = portabilityReasonsOf(hits);
|
|
958
|
+
const portable = reasons.length === 0;
|
|
902
959
|
if (portable !== row.portable)
|
|
903
960
|
return `skill row ${row.name} claims portable: ${String(row.portable)}, but its files derive ${String(portable)}`;
|
|
961
|
+
// The per-reason claim (F-079) is re-derived too — a row cannot name reasons its files do not carry.
|
|
962
|
+
if (row.portability !== undefined && !sameStrings(row.portability.reasons, reasons)) {
|
|
963
|
+
return `skill row ${row.name} claims portability reasons [${row.portability.reasons.join(', ')}], but its files derive [${reasons.join(', ')}]`;
|
|
964
|
+
}
|
|
904
965
|
}
|
|
905
966
|
// The copilot view as a WHOLE tree (codex round 9): EXACTLY `views/copilot/.github/skills/<name>/…`
|
|
906
967
|
// for the sorted portable rows — the files each row owns, the directories they imply — and nothing
|
|
@@ -1053,7 +1114,7 @@ export class SkillsStore {
|
|
|
1053
1114
|
if (!Array.isArray(s.skills))
|
|
1054
1115
|
return `${SNAPSHOT_MANIFEST_FILENAME} has no skills array`;
|
|
1055
1116
|
if (!s.skills.every(isSnapshotSkillRow)) {
|
|
1056
|
-
return `${SNAPSHOT_MANIFEST_FILENAME} has a skill row that is not {name: a wicked-garden-* skill name, dir: a safe relative skills/… path deriving that name, kind: ${[...SKILL_KINDS].join('|')}, core: boolean, portable: boolean, nested: what the dir spells} — metadata is never trusted to name a path, and core cannot judge seat compatibility from it`;
|
|
1117
|
+
return `${SNAPSHOT_MANIFEST_FILENAME} has a skill row that is not {name: a wicked-garden-* skill name, dir: a safe relative skills/… path deriving that name, kind: ${[...SKILL_KINDS].join('|')}, core: boolean, portable: boolean, portability?: {portable: the same boolean, reasons: sorted unique tokens (empty iff portable), evidence?: ≤ ${PORTABILITY_EVIDENCE_CAP} anchors}, nested: what the dir spells} — metadata is never trusted to name a path, and core cannot judge seat compatibility from it`;
|
|
1057
1118
|
}
|
|
1058
1119
|
if (!isSortedUniqueRows(s.skills))
|
|
1059
1120
|
return `${SNAPSHOT_MANIFEST_FILENAME} skill rows are not sorted by unique name — not what publish writes`;
|
|
@@ -1729,6 +1790,8 @@ export class SkillsStore {
|
|
|
1729
1790
|
const refused = [];
|
|
1730
1791
|
const catalogMd = new Map();
|
|
1731
1792
|
const byOwner = this.recordsByOwner(m);
|
|
1793
|
+
const skillDirs = this.ownershipDirs(m);
|
|
1794
|
+
const bundleView = this.publishedBundleView(m, skillDirs);
|
|
1732
1795
|
for (const [name, entry] of Object.entries(m.skills)) {
|
|
1733
1796
|
const records = byOwner.get(entry.dir) ?? [];
|
|
1734
1797
|
const userAdded = records.length > 0 && records.every(([, r]) => r.baselineHash === null);
|
|
@@ -1767,7 +1830,10 @@ export class SkillsStore {
|
|
|
1767
1830
|
// Judged from what is on disk as a REGULAR file reached without crossing a link: a recorded
|
|
1768
1831
|
// file that vanished is drift (publish reports it; the mutation in progress must not die on
|
|
1769
1832
|
// it); one that became a link, or sits behind one, is refused and reported — never read.
|
|
1770
|
-
|
|
1833
|
+
// EVERY reason is collected (F-079), against the bundle the next publish would carry — so
|
|
1834
|
+
// the snapshot row re-derives to the same reasons at verify.
|
|
1835
|
+
const ctx = bundleView.contextFor(entry.dir);
|
|
1836
|
+
const hits = [];
|
|
1771
1837
|
for (const [rel, r] of records) {
|
|
1772
1838
|
if (r.effectiveHash === null)
|
|
1773
1839
|
continue;
|
|
@@ -1781,10 +1847,12 @@ export class SkillsStore {
|
|
|
1781
1847
|
}
|
|
1782
1848
|
if (buf === null || looksBinary(buf))
|
|
1783
1849
|
continue;
|
|
1784
|
-
|
|
1785
|
-
|
|
1850
|
+
for (const hit of portabilityIssuesOf(buf.toString('utf8'), ctx(rel)))
|
|
1851
|
+
hits.push({ ...hit, fileRel: rel });
|
|
1786
1852
|
}
|
|
1787
|
-
|
|
1853
|
+
const reasons = portabilityReasonsOf(hits);
|
|
1854
|
+
entry.portable = reasons.length === 0;
|
|
1855
|
+
entry.portability = { portable: entry.portable, reasons, evidence: portabilityEvidenceOf(hits) };
|
|
1788
1856
|
}
|
|
1789
1857
|
// A DIRECTLY registered reference is core regardless of its readability (design v3.5 §5; codex
|
|
1790
1858
|
// round 8): a skill a workflow names by `skill_ref` keeps `core: true` when its `SKILL.md` is
|
|
@@ -1797,6 +1865,51 @@ export class SkillsStore {
|
|
|
1797
1865
|
entry.core = closure.core.has(name) || registered.has(name);
|
|
1798
1866
|
return refused;
|
|
1799
1867
|
}
|
|
1868
|
+
/**
|
|
1869
|
+
* The portability validator's view of the bundle (F-079): what the NEXT publish would carry —
|
|
1870
|
+
* support files inside the closure and ENABLED skills' own files. A disabled skill's files, a
|
|
1871
|
+
* file outside the closure, a vanished record: not there. Judging against this universe (rather
|
|
1872
|
+
* than everything under `effective/`) is what lets `snapshotRowsProblem` re-derive the SAME
|
|
1873
|
+
* reasons from the generation alone — a `../` link into a disabled skill is a broken link
|
|
1874
|
+
* (`unresolved-ref` at publish), not a portability fact about a file the snapshot omits. The
|
|
1875
|
+
* judged skill's own files are always visible to it (own-directory links stay portable anyway),
|
|
1876
|
+
* and `extra` lets a write guard see the files it is about to land.
|
|
1877
|
+
*/
|
|
1878
|
+
publishedBundleView(m, skillDirs, extra = {}) {
|
|
1879
|
+
const enabledDirs = new Set(Object.values(m.skills).filter((e) => e.enabled).map((e) => e.dir));
|
|
1880
|
+
const present = Object.entries(m.files).filter(([, r]) => r.effectiveHash !== null).map(([rel]) => rel);
|
|
1881
|
+
const visible = new Set();
|
|
1882
|
+
const own = new Map();
|
|
1883
|
+
for (const rel of present) {
|
|
1884
|
+
const owner = owningSkillDir(rel, skillDirs);
|
|
1885
|
+
if (owner === null) {
|
|
1886
|
+
// EXACTLY validate's selection of support files (review of #532, F-1): an owner-less file
|
|
1887
|
+
// under `skills/` (a `skills/README.md`, a dir whose SKILL.md never registered) is never
|
|
1888
|
+
// shipped, so it must not count as existing here either — or recompute would derive a
|
|
1889
|
+
// reason verify cannot re-derive from the generation, and `current` would be refused.
|
|
1890
|
+
if (!rel.startsWith(`${SKILLS_SUBDIR}/`) && inBundleClosure(rel))
|
|
1891
|
+
visible.add(rel);
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
if (enabledDirs.has(owner))
|
|
1895
|
+
visible.add(rel);
|
|
1896
|
+
const list = own.get(owner);
|
|
1897
|
+
if (list === undefined)
|
|
1898
|
+
own.set(owner, new Set([rel]));
|
|
1899
|
+
else
|
|
1900
|
+
list.add(rel);
|
|
1901
|
+
}
|
|
1902
|
+
for (const rel of Object.keys(extra))
|
|
1903
|
+
visible.add(rel);
|
|
1904
|
+
return {
|
|
1905
|
+
contextFor: (skillDir) => {
|
|
1906
|
+
const dirs = skillDirs.has(skillDir) ? skillDirs : new Set([...skillDirs, skillDir]);
|
|
1907
|
+
const mine = own.get(skillDir) ?? new Set();
|
|
1908
|
+
const exists = (p) => existsIn(visible, p) || existsIn(mine, p);
|
|
1909
|
+
return (fileRel) => ({ fileRel, skillDir, skillDirs: dirs, exists });
|
|
1910
|
+
},
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1800
1913
|
/** The recompute's refusals as WARNINGS — for a mutation that landed on another skill (the blocking form is publish's). */
|
|
1801
1914
|
recomputeWarnings(m) {
|
|
1802
1915
|
return this.recomputeDerived(m).map((f) => ({ ...f, severity: 'warning' }));
|
|
@@ -2024,6 +2137,10 @@ export class SkillsStore {
|
|
|
2024
2137
|
if (entry.enabled)
|
|
2025
2138
|
return this.result(m, name, findings);
|
|
2026
2139
|
entry.enabled = true;
|
|
2140
|
+
// Enablement moves the bundle the validator judges against (F-079): another skill's `../`
|
|
2141
|
+
// link into this one now resolves. Re-derive so the manifest's `portability` stays honest
|
|
2142
|
+
// between publishes (refusals were reported by the recompute above).
|
|
2143
|
+
this.recomputeDerived(m);
|
|
2027
2144
|
this.commit(m);
|
|
2028
2145
|
return this.result(m, name, findings);
|
|
2029
2146
|
}
|
|
@@ -2038,6 +2155,8 @@ export class SkillsStore {
|
|
|
2038
2155
|
if (!entry.enabled)
|
|
2039
2156
|
return this.result(m, name, findings);
|
|
2040
2157
|
entry.enabled = false;
|
|
2158
|
+
// See `enable`: the disabled skill's files leave the validator's bundle view.
|
|
2159
|
+
this.recomputeDerived(m);
|
|
2041
2160
|
this.commit(m);
|
|
2042
2161
|
return this.result(m, name, findings);
|
|
2043
2162
|
}
|
|
@@ -2163,7 +2282,7 @@ export class SkillsStore {
|
|
|
2163
2282
|
catch (err) {
|
|
2164
2283
|
return this.blocked(m, [this.pathFinding(err, name, rawRel)]);
|
|
2165
2284
|
}
|
|
2166
|
-
const findings = this.putGuards(name, entry, target.rel, content);
|
|
2285
|
+
const findings = this.putGuards(m, name, entry, target.rel, content);
|
|
2167
2286
|
if (verdictOf(findings) === 'blocked')
|
|
2168
2287
|
return this.blocked(m, findings);
|
|
2169
2288
|
// One file, the same transaction as every multi-file swap (codex round 7): staged, the existing
|
|
@@ -2509,16 +2628,32 @@ export class SkillsStore {
|
|
|
2509
2628
|
});
|
|
2510
2629
|
return this.result(m, name, findings);
|
|
2511
2630
|
}
|
|
2512
|
-
putGuards(name, entry, rel, content) {
|
|
2631
|
+
putGuards(m, name, entry, rel, content) {
|
|
2513
2632
|
const out = [];
|
|
2514
2633
|
if (rel === 'SKILL.md')
|
|
2515
2634
|
out.push(...frontmatterGuard(content, name, { isCore: entry.core, file: `${entry.dir}/SKILL.md` }));
|
|
2516
2635
|
out.push(...compact([nestedSkillCreateGuard(rel, name), supportFileGuard(rel, name)]));
|
|
2517
|
-
|
|
2518
|
-
|
|
2636
|
+
// Warn on every reason this write INTRODUCES — one the skill does not already carry (review
|
|
2637
|
+
// of #532, F-7): an author fixing a non-portable skill file by file still hears about a new
|
|
2638
|
+
// reason, while a reason the skill already has is not repeated on every edit. An entry with
|
|
2639
|
+
// no `portability` yet (an older manifest) knows no reasons, so everything is new.
|
|
2640
|
+
const known = new Set(entry.portability?.reasons ?? []);
|
|
2641
|
+
out.push(...nonPortableGuard({ [rel]: content }, name, this.writeContext(m, entry.dir, { [rel]: content })).filter((f) => f.portabilityReason === undefined || !known.has(f.portabilityReason)));
|
|
2519
2642
|
return out;
|
|
2520
2643
|
}
|
|
2521
|
-
|
|
2644
|
+
/**
|
|
2645
|
+
* The portability validator's context for a write of `files` (skill-relative keys) into
|
|
2646
|
+
* `skillDir`: the would-be bundle plus the files about to land, keyed by the skill-relative
|
|
2647
|
+
* path the guard iterates.
|
|
2648
|
+
*/
|
|
2649
|
+
writeContext(m, skillDir, files) {
|
|
2650
|
+
const extra = {};
|
|
2651
|
+
for (const [rel, content] of Object.entries(files))
|
|
2652
|
+
extra[`${skillDir}/${rel}`] = content;
|
|
2653
|
+
const ctx = this.publishedBundleView(m, this.ownershipDirs(m), extra).contextFor(skillDir);
|
|
2654
|
+
return (rel) => ctx(`${skillDir}/${rel}`);
|
|
2655
|
+
}
|
|
2656
|
+
filesGuards(m, name, dir, files) {
|
|
2522
2657
|
const out = [];
|
|
2523
2658
|
const keys = Object.keys(files).sort();
|
|
2524
2659
|
for (const rel of keys) {
|
|
@@ -2553,7 +2688,7 @@ export class SkillsStore {
|
|
|
2553
2688
|
continue;
|
|
2554
2689
|
out.push(finding('path-invalid', 'blocking', 'a files map lays each key out as a regular file, so a key cannot also be a directory on the way to another key — the two cannot both exist on disk', `${JSON.stringify(rel)} is a file AND a directory prefix of ${JSON.stringify(under)}`, { skill: name, file: rel }));
|
|
2555
2690
|
}
|
|
2556
|
-
out.push(...
|
|
2691
|
+
out.push(...nonPortableGuard(files, name, this.writeContext(m, dir, files)));
|
|
2557
2692
|
return out;
|
|
2558
2693
|
}
|
|
2559
2694
|
addGuards(m, name, files) {
|
|
@@ -2565,12 +2700,12 @@ export class SkillsStore {
|
|
|
2565
2700
|
if (existsSync(this.pluginPath(dir)) || this.hasBaselineDir(m, dir)) {
|
|
2566
2701
|
out.push(finding('name-collision', 'blocking', 'a directory already exists where this skill would land', `${dir} exists`, { skill: name }));
|
|
2567
2702
|
}
|
|
2568
|
-
out.push(...this.filesGuards(name, files));
|
|
2703
|
+
out.push(...this.filesGuards(m, name, dir, files));
|
|
2569
2704
|
out.push(...frontmatterGuard(files['SKILL.md'], name, { isCore: this.registeredRefs().has(name), file: `${dir}/SKILL.md` }));
|
|
2570
2705
|
return out;
|
|
2571
2706
|
}
|
|
2572
2707
|
replaceGuards(m, name, entry, files) {
|
|
2573
|
-
const out = this.filesGuards(name, files);
|
|
2708
|
+
const out = this.filesGuards(m, name, entry.dir, files);
|
|
2574
2709
|
const nestedUnder = [...this.ownershipDirs(m)].filter((d) => d !== entry.dir && d.startsWith(`${entry.dir}/`));
|
|
2575
2710
|
for (const rel of Object.keys(files)) {
|
|
2576
2711
|
const owner = owningSkillDir(`${entry.dir}/${rel}`, new Set([entry.dir, ...nestedUnder]));
|
|
@@ -3047,6 +3182,9 @@ export class SkillsStore {
|
|
|
3047
3182
|
kind: entry.kind,
|
|
3048
3183
|
core: entry.core,
|
|
3049
3184
|
portable: entry.portable,
|
|
3185
|
+
// `validate` recomputed every entry (F-079), so the per-reason claim is fresh here; the
|
|
3186
|
+
// fallback only guards the type — a row is never written without `portability`.
|
|
3187
|
+
portability: entry.portability ?? { portable: entry.portable, reasons: [], evidence: [] },
|
|
3050
3188
|
nested: isNestedSkillDir(entry.dir),
|
|
3051
3189
|
}))
|
|
3052
3190
|
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)),
|