Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .changeset/6470-gantt-dependency-field-deprecated.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
---
'@object-ui/types': patch
'@object-ui/plugin-gantt': patch
---

`ObjectGanttSchema.dependencyField` is now marked `@deprecated` on both published
declaration faces, naming `dependenciesField` as the canonical spelling
(objectui#6470). Nothing is removed and nothing is rejected that was accepted
before.

**What the two spellings were.** `ObjectGanttSchema` declares both, and
`getGanttConfig`'s flat branch reads them with a `||`:
`dependenciesField: schema.dependenciesField || schema.dependencyField`.
`dependenciesField` is the spec's key (`@objectstack/spec`
`GanttConfigSchema.dependenciesField`); the singular `dependencyField` has NO
spec counterpart — zero occurrences across `packages/spec/src`, measured against
a live positive control on the plural. Until objectui#6051 declared the plural,
the singular was the ONLY dependencies spelling this interface carried, so for
the whole time the alias existed the published type taught the non-spec key and
hid the canonical one.

**What was missing was the ranking, not the behaviour.** The two were declared as
equals: nothing on either face said which one to author, so a reader — including
an AI writing metadata, which is the reader this project optimises for — had a
coin flip between a spec key and pre-spec vocabulary. The marker turns that coin
flip into a fact the type itself carries, and the zod mirror's description makes
it readable at runtime as well as in an editor.

This adopts the idiom already ruled for this exact shape rather than inventing a
second one: `KanbanConfig`'s pre-#2231 aliases (`groupField`, `cardFields`) carry
`/** @deprecated legacy alias for the spec's X */` plus
`.describe('Deprecated alias for X')`, and `dependencyField` now reads the same
way.

**⛔ Not a removal, deliberately.** Deleting the alias — or narrowing the
renderer's `||` — would break every author who wrote the singular and narrow the
accept set of a published surface. That is a maintainer decision on a future
enforce-or-remove card once the deprecation has sat a release, and it is
explicitly excluded here. Two pins hold the line in both directions:
`packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts`
fails if the marker goes missing AND if the alias stops being declared or
accepted, and `packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx`
fails if the `||` limb is dropped — the two spellings must keep resolving to the
same config, with the canonical one winning when both carry a value.

`packages/plugin-gantt/README.md`'s `ObjectGanttSchema` example authored the
singular; it was the only in-repo site that did, and it now authors the plural
with the alias named as legacy. No runtime code, fixture, example app or catalog
schema authored it.
7 changes: 6 additions & 1 deletion packages/plugin-gantt/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -670,10 +670,15 @@ const gantt: ObjectGanttSchema = {
startDateField: 'start_date',
endDateField: 'end_date',
progressField: 'completion_percentage',
dependencyField: 'dependent_task_ids',
dependenciesField: 'dependent_task_ids',
};
```

`dependenciesField` (plural) is the spec's spelling and the one to author. The
singular `dependencyField` is a `@deprecated` legacy alias: `ObjectGantt` still
reads it (`dependenciesField || dependencyField`), so existing metadata keeps
working, but new metadata should not use it.

For a list view served under the `gantt` view type, the same configuration is a
`gantt` block on `ListViewSchema` (typed by `GanttConfig`, also from
`@object-ui/types`) rather than top-level keys — `ObjectGantt` reads either
Expand Down
176 changes: 176 additions & 0 deletions packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* Behaviour pin — deprecating `dependencyField` changed NOTHING at the read site
* (objectui#6470).
*
* ## What this file is for
*
* objectui#6470 marks the singular `dependencyField` `@deprecated` on both
* published declaration faces (`packages/types/src/objectql.ts` and its zod
* mirror), naming the spec's `dependenciesField` as canonical. The marker is
* documentation; `getGanttConfig`'s flat branch still reads
* `dependenciesField || dependencyField` and every author who wrote the singular
* must keep rendering exactly as before.
*
* ⛔ Removal was explicitly excluded from that card and deferred to a future
* enforce-or-remove decision. This suite is what makes the exclusion mechanical:
* drop the `|| schema.dependencyField` limb and the first two cases below go red
* naming the break, rather than a published surface silently losing an accepted
* spelling.
*
* ## Why the two spellings name DIFFERENT record fields here
*
* A fixture where both spellings point at the same field can only prove that
* SOMETHING resolved; it cannot show WHICH limb of the `||` produced it, so it
* would stay green with either limb deleted. Pointing them at different fields
* (`legacy_preds` vs `canonical_preds`, with values that cannot be confused)
* makes the resolved config visible in the rendered tasks — the same technique
* `ObjectGantt.blockPrecedence.test.tsx` uses for the block/flat flip.
*
* The "same config" half the card asks for is then pinned directly, by the
* third case: the two spellings aimed at ONE field produce byte-identical task
* dependencies.
*
* ## The observable
*
* `dependenciesField` is not passed to `GanttView` as a config value — it is
* consumed on the way in (`dependencies: normalizeDependencies(record[dep])`)
* and again as the `autoSchedule` / `onDependencyCreate` switches. Both are
* observed: the per-task dependency ids say WHICH field was read, and
* `autoSchedule` says the config resolved to a truthy field name at all.
*/
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { ObjectGantt } from './ObjectGantt';

vi.mock('./GanttView', () => ({
GanttView: ({ tasks, autoSchedule }: any) => (
<div
data-testid="gantt-view"
data-count={tasks.length}
data-deps={tasks.map((t: any) => t.dependencies.join('+')).join('|')}
data-autoschedule={String(!!autoSchedule)}
/>
),
}));

vi.mock('./ResourceWorkload', () => ({
ResourceWorkload: ({ tasks }: any) => <div data-testid="resource-workload" data-count={tasks.length} />,
}));

/**
* One record carrying BOTH candidate fields, with values that cannot be
* confused: the legacy field's ids start `L`, the canonical field's start `C`.
*/
const INLINE = [
{
id: '1',
name: 'Alpha',
start: '2024-01-01',
end: '2024-01-05',
legacy_preds: 'L1,L2',
canonical_preds: 'C1,C2',
shared_preds: 'S1,S2',
},
];

const DATES = { startDateField: 'start', endDateField: 'end', titleField: 'name' };

async function rendered(extra: Record<string, unknown>, objectName: string) {
const { container } = render(
<ObjectGantt
schema={
{
type: 'object-gantt',
objectName,
...DATES,
data: { provider: 'value', items: INLINE },
...extra,
} as any
}
/>,
);
const el = () => container.querySelector('[data-testid="gantt-view"]') as HTMLElement;
await waitFor(() => expect(el()?.getAttribute('data-count')).toBe('1'));
return {
deps: el().getAttribute('data-deps'),
autoSchedule: el().getAttribute('data-autoschedule'),
};
}

describe('both dependency spellings still resolve to the same config (objectui#6470)', () => {
it('the DEPRECATED singular is still read', async () => {
// ⛔ The removal guard. Deleting `|| schema.dependencyField` from
// `getGanttConfig` turns this red — every author who wrote the singular
// loses their dependency links, and `autoSchedule` silently switches off
// with them.
const { deps, autoSchedule } = await rendered(
{ dependencyField: 'legacy_preds' },
'alias_singular',
);
expect(deps).toBe('L1+L2');
expect(autoSchedule).toBe('true');
});

it('the CANONICAL plural is read', async () => {
// The counter-probe: the assertion above must be about the singular limb,
// not about dependencies working at all.
const { deps, autoSchedule } = await rendered(
{ dependenciesField: 'canonical_preds' },
'alias_plural',
);
expect(deps).toBe('C1+C2');
expect(autoSchedule).toBe('true');
});

it('aimed at ONE field, the two spellings produce an identical result', async () => {
// The card's acceptance criterion, stated directly: the deprecation is a
// marker, so the singular and the plural are interchangeable at the read
// site. Compared value-to-value rather than each against a literal, so this
// stays true however `normalizeDependencies` evolves.
const viaLegacy = await rendered({ dependencyField: 'shared_preds' }, 'alias_same_a');
const viaCanonical = await rendered({ dependenciesField: 'shared_preds' }, 'alias_same_b');
expect(viaLegacy).toEqual(viaCanonical);
expect(viaLegacy.deps).toBe('S1+S2');
});

it('the canonical spelling WINS when a node carries both', async () => {
// Precedence, and the reason the deprecation marker is truthful: an author
// migrating key-by-key gets the canonical value, not a merge and not the
// legacy one.
const { deps } = await rendered(
{ dependenciesField: 'canonical_preds', dependencyField: 'legacy_preds' },
'alias_both',
);
expect(deps).toBe('C1+C2');
});

it('an EMPTY canonical value falls through to the alias — a property of `||`', async () => {
// Recorded, not designed. `dependenciesField: ''` is falsy, so the `||`
// reaches the singular; `??` would not. Pinned so that a future rewrite of
// that expression is a decision rather than an accident — and so the
// enforce-or-remove card inherits the real accept semantics instead of
// re-deriving them.
const { deps } = await rendered(
{ dependenciesField: '', dependencyField: 'legacy_preds' },
'alias_empty_canonical',
);
expect(deps).toBe('L1+L2');
});

it('neither spelling authored: no dependencies, and auto-scheduling stays OFF', async () => {
// Non-vacuity for every case above — without this, a harness that always
// reported the same string would pass them all.
const { deps, autoSchedule } = await rendered({}, 'alias_none');
expect(deps).toBe('');
expect(autoSchedule).toBe('false');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .changeset/6470-gantt-dependency-field-deprecated.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
---
'@object-ui/types': patch
'@object-ui/plugin-gantt': patch
---

`ObjectGanttSchema.dependencyField` is now marked `@deprecated` on both published
declaration faces, naming `dependenciesField` as the canonical spelling
(objectui#6470). Nothing is removed and nothing is rejected that was accepted
before.

**What the two spellings were.** `ObjectGanttSchema` declares both, and
`getGanttConfig`'s flat branch reads them with a `||`:
`dependenciesField: schema.dependenciesField || schema.dependencyField`.
`dependenciesField` is the spec's key (`@objectstack/spec`
`GanttConfigSchema.dependenciesField`); the singular `dependencyField` has NO
spec counterpart — zero occurrences across `packages/spec/src`, measured against
a live positive control on the plural. Until objectui#6051 declared the plural,
the singular was the ONLY dependencies spelling this interface carried, so for
the whole time the alias existed the published type taught the non-spec key and
hid the canonical one.

**What was missing was the ranking, not the behaviour.** The two were declared as
equals: nothing on either face said which one to author, so a reader — including
an AI writing metadata, which is the reader this project optimises for — had a
coin flip between a spec key and pre-spec vocabulary. The marker turns that coin
flip into a fact the type itself carries, and the zod mirror's description makes
it readable at runtime as well as in an editor.

This adopts the idiom already ruled for this exact shape rather than inventing a
second one: `KanbanConfig`'s pre-#2231 aliases (`groupField`, `cardFields`) carry
`/** @deprecated legacy alias for the spec's X */` plus
`.describe('Deprecated alias for X')`, and `dependencyField` now reads the same
way.

**⛔ Not a removal, deliberately.** Deleting the alias — or narrowing the
renderer's `||` — would break every author who wrote the singular and narrow the
accept set of a published surface. That is a maintainer decision on a future
enforce-or-remove card once the deprecation has sat a release, and it is
explicitly excluded here. Two pins hold the line in both directions:
`packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts`
fails if the marker goes missing AND if the alias stops being declared or
accepted, and `packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx`
fails if the `||` limb is dropped — the two spellings must keep resolving to the
same config, with the canonical one winning when both carry a value.

`packages/plugin-gantt/README.md`'s `ObjectGanttSchema` example authored the
singular; it was the only in-repo site that did, and it now authors the plural
with the alias named as legacy. No runtime code, fixture, example app or catalog
schema authored it.
7 changes: 6 additions & 1 deletion packages/plugin-gantt/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -670,10 +670,15 @@ const gantt: ObjectGanttSchema = {
startDateField: 'start_date',
endDateField: 'end_date',
progressField: 'completion_percentage',
dependencyField: 'dependent_task_ids',
dependenciesField: 'dependent_task_ids',
};
```

`dependenciesField` (plural) is the spec's spelling and the one to author. The
singular `dependencyField` is a `@deprecated` legacy alias: `ObjectGantt` still
reads it (`dependenciesField || dependencyField`), so existing metadata keeps
working, but new metadata should not use it.

For a list view served under the `gantt` view type, the same configuration is a
`gantt` block on `ListViewSchema` (typed by `GanttConfig`, also from
`@object-ui/types`) rather than top-level keys — `ObjectGantt` reads either
Expand Down
176 changes: 176 additions & 0 deletions packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* Behaviour pin — deprecating `dependencyField` changed NOTHING at the read site
* (objectui#6470).
*
* ## What this file is for
*
* objectui#6470 marks the singular `dependencyField` `@deprecated` on both
* published declaration faces (`packages/types/src/objectql.ts` and its zod
* mirror), naming the spec's `dependenciesField` as canonical. The marker is
* documentation; `getGanttConfig`'s flat branch still reads
* `dependenciesField || dependencyField` and every author who wrote the singular
* must keep rendering exactly as before.
*
* ⛔ Removal was explicitly excluded from that card and deferred to a future
* enforce-or-remove decision. This suite is what makes the exclusion mechanical:
* drop the `|| schema.dependencyField` limb and the first two cases below go red
* naming the break, rather than a published surface silently losing an accepted
* spelling.
*
* ## Why the two spellings name DIFFERENT record fields here
*
* A fixture where both spellings point at the same field can only prove that
* SOMETHING resolved; it cannot show WHICH limb of the `||` produced it, so it
* would stay green with either limb deleted. Pointing them at different fields
* (`legacy_preds` vs `canonical_preds`, with values that cannot be confused)
* makes the resolved config visible in the rendered tasks — the same technique
* `ObjectGantt.blockPrecedence.test.tsx` uses for the block/flat flip.
*
* The "same config" half the card asks for is then pinned directly, by the
* third case: the two spellings aimed at ONE field produce byte-identical task
* dependencies.
*
* ## The observable
*
* `dependenciesField` is not passed to `GanttView` as a config value — it is
* consumed on the way in (`dependencies: normalizeDependencies(record[dep])`)
* and again as the `autoSchedule` / `onDependencyCreate` switches. Both are
* observed: the per-task dependency ids say WHICH field was read, and
* `autoSchedule` says the config resolved to a truthy field name at all.
*/
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { ObjectGantt } from './ObjectGantt';

vi.mock('./GanttView', () => ({
GanttView: ({ tasks, autoSchedule }: any) => (
<div
data-testid="gantt-view"
data-count={tasks.length}
data-deps={tasks.map((t: any) => t.dependencies.join('+')).join('|')}
data-autoschedule={String(!!autoSchedule)}
/>
),
}));

vi.mock('./ResourceWorkload', () => ({
ResourceWorkload: ({ tasks }: any) => <div data-testid="resource-workload" data-count={tasks.length} />,
}));

/**
* One record carrying BOTH candidate fields, with values that cannot be
* confused: the legacy field's ids start `L`, the canonical field's start `C`.
*/
const INLINE = [
{
id: '1',
name: 'Alpha',
start: '2024-01-01',
end: '2024-01-05',
legacy_preds: 'L1,L2',
canonical_preds: 'C1,C2',
shared_preds: 'S1,S2',
},
];

const DATES = { startDateField: 'start', endDateField: 'end', titleField: 'name' };

async function rendered(extra: Record<string, unknown>, objectName: string) {
const { container } = render(
<ObjectGantt
schema={
{
type: 'object-gantt',
objectName,
...DATES,
data: { provider: 'value', items: INLINE },
...extra,
} as any
}
/>,
);
const el = () => container.querySelector('[data-testid="gantt-view"]') as HTMLElement;
await waitFor(() => expect(el()?.getAttribute('data-count')).toBe('1'));
return {
deps: el().getAttribute('data-deps'),
autoSchedule: el().getAttribute('data-autoschedule'),
};
}

describe('both dependency spellings still resolve to the same config (objectui#6470)', () => {
it('the DEPRECATED singular is still read', async () => {
// ⛔ The removal guard. Deleting `|| schema.dependencyField` from
// `getGanttConfig` turns this red — every author who wrote the singular
// loses their dependency links, and `autoSchedule` silently switches off
// with them.
const { deps, autoSchedule } = await rendered(
{ dependencyField: 'legacy_preds' },
'alias_singular',
);
expect(deps).toBe('L1+L2');
expect(autoSchedule).toBe('true');
});

it('the CANONICAL plural is read', async () => {
// The counter-probe: the assertion above must be about the singular limb,
// not about dependencies working at all.
const { deps, autoSchedule } = await rendered(
{ dependenciesField: 'canonical_preds' },
'alias_plural',
);
expect(deps).toBe('C1+C2');
expect(autoSchedule).toBe('true');
});

it('aimed at ONE field, the two spellings produce an identical result', async () => {
// The card's acceptance criterion, stated directly: the deprecation is a
// marker, so the singular and the plural are interchangeable at the read
// site. Compared value-to-value rather than each against a literal, so this
// stays true however `normalizeDependencies` evolves.
const viaLegacy = await rendered({ dependencyField: 'shared_preds' }, 'alias_same_a');
const viaCanonical = await rendered({ dependenciesField: 'shared_preds' }, 'alias_same_b');
expect(viaLegacy).toEqual(viaCanonical);
expect(viaLegacy.deps).toBe('S1+S2');
});

it('the canonical spelling WINS when a node carries both', async () => {
// Precedence, and the reason the deprecation marker is truthful: an author
// migrating key-by-key gets the canonical value, not a merge and not the
// legacy one.
const { deps } = await rendered(
{ dependenciesField: 'canonical_preds', dependencyField: 'legacy_preds' },
'alias_both',
);
expect(deps).toBe('C1+C2');
});

it('an EMPTY canonical value falls through to the alias — a property of `||`', async () => {
// Recorded, not designed. `dependenciesField: ''` is falsy, so the `||`
// reaches the singular; `??` would not. Pinned so that a future rewrite of
// that expression is a decision rather than an accident — and so the
// enforce-or-remove card inherits the real accept semantics instead of
// re-deriving them.
const { deps } = await rendered(
{ dependenciesField: '', dependencyField: 'legacy_preds' },
'alias_empty_canonical',
);
expect(deps).toBe('L1+L2');
});

it('neither spelling authored: no dependencies, and auto-scheduling stays OFF', async () => {
// Non-vacuity for every case above — without this, a harness that always
// reported the same string would pass them all.
const { deps, autoSchedule } = await rendered({}, 'alias_none');
expect(deps).toBe('');
expect(autoSchedule).toBe('false');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .changeset/6470-gantt-dependency-field-deprecated.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
---
'@object-ui/types': patch
'@object-ui/plugin-gantt': patch
---

`ObjectGanttSchema.dependencyField` is now marked `@deprecated` on both published
declaration faces, naming `dependenciesField` as the canonical spelling
(objectui#6470). Nothing is removed and nothing is rejected that was accepted
before.

**What the two spellings were.** `ObjectGanttSchema` declares both, and
`getGanttConfig`'s flat branch reads them with a `||`:
`dependenciesField: schema.dependenciesField || schema.dependencyField`.
`dependenciesField` is the spec's key (`@objectstack/spec`
`GanttConfigSchema.dependenciesField`); the singular `dependencyField` has NO
spec counterpart — zero occurrences across `packages/spec/src`, measured against
a live positive control on the plural. Until objectui#6051 declared the plural,
the singular was the ONLY dependencies spelling this interface carried, so for
the whole time the alias existed the published type taught the non-spec key and
hid the canonical one.

**What was missing was the ranking, not the behaviour.** The two were declared as
equals: nothing on either face said which one to author, so a reader — including
an AI writing metadata, which is the reader this project optimises for — had a
coin flip between a spec key and pre-spec vocabulary. The marker turns that coin
flip into a fact the type itself carries, and the zod mirror's description makes
it readable at runtime as well as in an editor.

This adopts the idiom already ruled for this exact shape rather than inventing a
second one: `KanbanConfig`'s pre-#2231 aliases (`groupField`, `cardFields`) carry
`/** @deprecated legacy alias for the spec's X */` plus
`.describe('Deprecated alias for X')`, and `dependencyField` now reads the same
way.

**⛔ Not a removal, deliberately.** Deleting the alias — or narrowing the
renderer's `||` — would break every author who wrote the singular and narrow the
accept set of a published surface. That is a maintainer decision on a future
enforce-or-remove card once the deprecation has sat a release, and it is
explicitly excluded here. Two pins hold the line in both directions:
`packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts`
fails if the marker goes missing AND if the alias stops being declared or
accepted, and `packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx`
fails if the `||` limb is dropped — the two spellings must keep resolving to the
same config, with the canonical one winning when both carry a value.

`packages/plugin-gantt/README.md`'s `ObjectGanttSchema` example authored the
singular; it was the only in-repo site that did, and it now authors the plural
with the alias named as legacy. No runtime code, fixture, example app or catalog
schema authored it.
7 changes: 6 additions & 1 deletion packages/plugin-gantt/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -670,10 +670,15 @@ const gantt: ObjectGanttSchema = {
startDateField: 'start_date',
endDateField: 'end_date',
progressField: 'completion_percentage',
dependencyField: 'dependent_task_ids',
dependenciesField: 'dependent_task_ids',
};
```

`dependenciesField` (plural) is the spec's spelling and the one to author. The
singular `dependencyField` is a `@deprecated` legacy alias: `ObjectGantt` still
reads it (`dependenciesField || dependencyField`), so existing metadata keeps
working, but new metadata should not use it.

For a list view served under the `gantt` view type, the same configuration is a
`gantt` block on `ListViewSchema` (typed by `GanttConfig`, also from
`@object-ui/types`) rather than top-level keys — `ObjectGantt` reads either
Expand Down
176 changes: 176 additions & 0 deletions packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* Behaviour pin — deprecating `dependencyField` changed NOTHING at the read site
* (objectui#6470).
*
* ## What this file is for
*
* objectui#6470 marks the singular `dependencyField` `@deprecated` on both
* published declaration faces (`packages/types/src/objectql.ts` and its zod
* mirror), naming the spec's `dependenciesField` as canonical. The marker is
* documentation; `getGanttConfig`'s flat branch still reads
* `dependenciesField || dependencyField` and every author who wrote the singular
* must keep rendering exactly as before.
*
* ⛔ Removal was explicitly excluded from that card and deferred to a future
* enforce-or-remove decision. This suite is what makes the exclusion mechanical:
* drop the `|| schema.dependencyField` limb and the first two cases below go red
* naming the break, rather than a published surface silently losing an accepted
* spelling.
*
* ## Why the two spellings name DIFFERENT record fields here
*
* A fixture where both spellings point at the same field can only prove that
* SOMETHING resolved; it cannot show WHICH limb of the `||` produced it, so it
* would stay green with either limb deleted. Pointing them at different fields
* (`legacy_preds` vs `canonical_preds`, with values that cannot be confused)
* makes the resolved config visible in the rendered tasks — the same technique
* `ObjectGantt.blockPrecedence.test.tsx` uses for the block/flat flip.
*
* The "same config" half the card asks for is then pinned directly, by the
* third case: the two spellings aimed at ONE field produce byte-identical task
* dependencies.
*
* ## The observable
*
* `dependenciesField` is not passed to `GanttView` as a config value — it is
* consumed on the way in (`dependencies: normalizeDependencies(record[dep])`)
* and again as the `autoSchedule` / `onDependencyCreate` switches. Both are
* observed: the per-task dependency ids say WHICH field was read, and
* `autoSchedule` says the config resolved to a truthy field name at all.
*/
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { ObjectGantt } from './ObjectGantt';

vi.mock('./GanttView', () => ({
GanttView: ({ tasks, autoSchedule }: any) => (
<div
data-testid="gantt-view"
data-count={tasks.length}
data-deps={tasks.map((t: any) => t.dependencies.join('+')).join('|')}
data-autoschedule={String(!!autoSchedule)}
/>
),
}));

vi.mock('./ResourceWorkload', () => ({
ResourceWorkload: ({ tasks }: any) => <div data-testid="resource-workload" data-count={tasks.length} />,
}));

/**
* One record carrying BOTH candidate fields, with values that cannot be
* confused: the legacy field's ids start `L`, the canonical field's start `C`.
*/
const INLINE = [
{
id: '1',
name: 'Alpha',
start: '2024-01-01',
end: '2024-01-05',
legacy_preds: 'L1,L2',
canonical_preds: 'C1,C2',
shared_preds: 'S1,S2',
},
];

const DATES = { startDateField: 'start', endDateField: 'end', titleField: 'name' };

async function rendered(extra: Record<string, unknown>, objectName: string) {
const { container } = render(
<ObjectGantt
schema={
{
type: 'object-gantt',
objectName,
...DATES,
data: { provider: 'value', items: INLINE },
...extra,
} as any
}
/>,
);
const el = () => container.querySelector('[data-testid="gantt-view"]') as HTMLElement;
await waitFor(() => expect(el()?.getAttribute('data-count')).toBe('1'));
return {
deps: el().getAttribute('data-deps'),
autoSchedule: el().getAttribute('data-autoschedule'),
};
}

describe('both dependency spellings still resolve to the same config (objectui#6470)', () => {
it('the DEPRECATED singular is still read', async () => {
// ⛔ The removal guard. Deleting `|| schema.dependencyField` from
// `getGanttConfig` turns this red — every author who wrote the singular
// loses their dependency links, and `autoSchedule` silently switches off
// with them.
const { deps, autoSchedule } = await rendered(
{ dependencyField: 'legacy_preds' },
'alias_singular',
);
expect(deps).toBe('L1+L2');
expect(autoSchedule).toBe('true');
});

it('the CANONICAL plural is read', async () => {
// The counter-probe: the assertion above must be about the singular limb,
// not about dependencies working at all.
const { deps, autoSchedule } = await rendered(
{ dependenciesField: 'canonical_preds' },
'alias_plural',
);
expect(deps).toBe('C1+C2');
expect(autoSchedule).toBe('true');
});

it('aimed at ONE field, the two spellings produce an identical result', async () => {
// The card's acceptance criterion, stated directly: the deprecation is a
// marker, so the singular and the plural are interchangeable at the read
// site. Compared value-to-value rather than each against a literal, so this
// stays true however `normalizeDependencies` evolves.
const viaLegacy = await rendered({ dependencyField: 'shared_preds' }, 'alias_same_a');
const viaCanonical = await rendered({ dependenciesField: 'shared_preds' }, 'alias_same_b');
expect(viaLegacy).toEqual(viaCanonical);
expect(viaLegacy.deps).toBe('S1+S2');
});

it('the canonical spelling WINS when a node carries both', async () => {
// Precedence, and the reason the deprecation marker is truthful: an author
// migrating key-by-key gets the canonical value, not a merge and not the
// legacy one.
const { deps } = await rendered(
{ dependenciesField: 'canonical_preds', dependencyField: 'legacy_preds' },
'alias_both',
);
expect(deps).toBe('C1+C2');
});

it('an EMPTY canonical value falls through to the alias — a property of `||`', async () => {
// Recorded, not designed. `dependenciesField: ''` is falsy, so the `||`
// reaches the singular; `??` would not. Pinned so that a future rewrite of
// that expression is a decision rather than an accident — and so the
// enforce-or-remove card inherits the real accept semantics instead of
// re-deriving them.
const { deps } = await rendered(
{ dependenciesField: '', dependencyField: 'legacy_preds' },
'alias_empty_canonical',
);
expect(deps).toBe('L1+L2');
});

it('neither spelling authored: no dependencies, and auto-scheduling stays OFF', async () => {
// Non-vacuity for every case above — without this, a harness that always
// reported the same string would pass them all.
const { deps, autoSchedule } = await rendered({}, 'alias_none');
expect(deps).toBe('');
expect(autoSchedule).toBe('false');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .changeset/6470-gantt-dependency-field-deprecated.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
---
'@object-ui/types': patch
'@object-ui/plugin-gantt': patch
---

`ObjectGanttSchema.dependencyField` is now marked `@deprecated` on both published
declaration faces, naming `dependenciesField` as the canonical spelling
(objectui#6470). Nothing is removed and nothing is rejected that was accepted
before.

**What the two spellings were.** `ObjectGanttSchema` declares both, and
`getGanttConfig`'s flat branch reads them with a `||`:
`dependenciesField: schema.dependenciesField || schema.dependencyField`.
`dependenciesField` is the spec's key (`@objectstack/spec`
`GanttConfigSchema.dependenciesField`); the singular `dependencyField` has NO
spec counterpart — zero occurrences across `packages/spec/src`, measured against
a live positive control on the plural. Until objectui#6051 declared the plural,
the singular was the ONLY dependencies spelling this interface carried, so for
the whole time the alias existed the published type taught the non-spec key and
hid the canonical one.

**What was missing was the ranking, not the behaviour.** The two were declared as
equals: nothing on either face said which one to author, so a reader — including
an AI writing metadata, which is the reader this project optimises for — had a
coin flip between a spec key and pre-spec vocabulary. The marker turns that coin
flip into a fact the type itself carries, and the zod mirror's description makes
it readable at runtime as well as in an editor.

This adopts the idiom already ruled for this exact shape rather than inventing a
second one: `KanbanConfig`'s pre-#2231 aliases (`groupField`, `cardFields`) carry
`/** @deprecated legacy alias for the spec's X */` plus
`.describe('Deprecated alias for X')`, and `dependencyField` now reads the same
way.

**⛔ Not a removal, deliberately.** Deleting the alias — or narrowing the
renderer's `||` — would break every author who wrote the singular and narrow the
accept set of a published surface. That is a maintainer decision on a future
enforce-or-remove card once the deprecation has sat a release, and it is
explicitly excluded here. Two pins hold the line in both directions:
`packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts`
fails if the marker goes missing AND if the alias stops being declared or
accepted, and `packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx`
fails if the `||` limb is dropped — the two spellings must keep resolving to the
same config, with the canonical one winning when both carry a value.

`packages/plugin-gantt/README.md`'s `ObjectGanttSchema` example authored the
singular; it was the only in-repo site that did, and it now authors the plural
with the alias named as legacy. No runtime code, fixture, example app or catalog
schema authored it.
7 changes: 6 additions & 1 deletion packages/plugin-gantt/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -670,10 +670,15 @@ const gantt: ObjectGanttSchema = {
startDateField: 'start_date',
endDateField: 'end_date',
progressField: 'completion_percentage',
dependencyField: 'dependent_task_ids',
dependenciesField: 'dependent_task_ids',
};
```

`dependenciesField` (plural) is the spec's spelling and the one to author. The
singular `dependencyField` is a `@deprecated` legacy alias: `ObjectGantt` still
reads it (`dependenciesField || dependencyField`), so existing metadata keeps
working, but new metadata should not use it.

For a list view served under the `gantt` view type, the same configuration is a
`gantt` block on `ListViewSchema` (typed by `GanttConfig`, also from
`@object-ui/types`) rather than top-level keys — `ObjectGantt` reads either
Expand Down
176 changes: 176 additions & 0 deletions packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* Behaviour pin — deprecating `dependencyField` changed NOTHING at the read site
* (objectui#6470).
*
* ## What this file is for
*
* objectui#6470 marks the singular `dependencyField` `@deprecated` on both
* published declaration faces (`packages/types/src/objectql.ts` and its zod
* mirror), naming the spec's `dependenciesField` as canonical. The marker is
* documentation; `getGanttConfig`'s flat branch still reads
* `dependenciesField || dependencyField` and every author who wrote the singular
* must keep rendering exactly as before.
*
* ⛔ Removal was explicitly excluded from that card and deferred to a future
* enforce-or-remove decision. This suite is what makes the exclusion mechanical:
* drop the `|| schema.dependencyField` limb and the first two cases below go red
* naming the break, rather than a published surface silently losing an accepted
* spelling.
*
* ## Why the two spellings name DIFFERENT record fields here
*
* A fixture where both spellings point at the same field can only prove that
* SOMETHING resolved; it cannot show WHICH limb of the `||` produced it, so it
* would stay green with either limb deleted. Pointing them at different fields
* (`legacy_preds` vs `canonical_preds`, with values that cannot be confused)
* makes the resolved config visible in the rendered tasks — the same technique
* `ObjectGantt.blockPrecedence.test.tsx` uses for the block/flat flip.
*
* The "same config" half the card asks for is then pinned directly, by the
* third case: the two spellings aimed at ONE field produce byte-identical task
* dependencies.
*
* ## The observable
*
* `dependenciesField` is not passed to `GanttView` as a config value — it is
* consumed on the way in (`dependencies: normalizeDependencies(record[dep])`)
* and again as the `autoSchedule` / `onDependencyCreate` switches. Both are
* observed: the per-task dependency ids say WHICH field was read, and
* `autoSchedule` says the config resolved to a truthy field name at all.
*/
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { ObjectGantt } from './ObjectGantt';

vi.mock('./GanttView', () => ({
GanttView: ({ tasks, autoSchedule }: any) => (
<div
data-testid="gantt-view"
data-count={tasks.length}
data-deps={tasks.map((t: any) => t.dependencies.join('+')).join('|')}
data-autoschedule={String(!!autoSchedule)}
/>
),
}));

vi.mock('./ResourceWorkload', () => ({
ResourceWorkload: ({ tasks }: any) => <div data-testid="resource-workload" data-count={tasks.length} />,
}));

/**
* One record carrying BOTH candidate fields, with values that cannot be
* confused: the legacy field's ids start `L`, the canonical field's start `C`.
*/
const INLINE = [
{
id: '1',
name: 'Alpha',
start: '2024-01-01',
end: '2024-01-05',
legacy_preds: 'L1,L2',
canonical_preds: 'C1,C2',
shared_preds: 'S1,S2',
},
];

const DATES = { startDateField: 'start', endDateField: 'end', titleField: 'name' };

async function rendered(extra: Record<string, unknown>, objectName: string) {
const { container } = render(
<ObjectGantt
schema={
{
type: 'object-gantt',
objectName,
...DATES,
data: { provider: 'value', items: INLINE },
...extra,
} as any
}
/>,
);
const el = () => container.querySelector('[data-testid="gantt-view"]') as HTMLElement;
await waitFor(() => expect(el()?.getAttribute('data-count')).toBe('1'));
return {
deps: el().getAttribute('data-deps'),
autoSchedule: el().getAttribute('data-autoschedule'),
};
}

describe('both dependency spellings still resolve to the same config (objectui#6470)', () => {
it('the DEPRECATED singular is still read', async () => {
// ⛔ The removal guard. Deleting `|| schema.dependencyField` from
// `getGanttConfig` turns this red — every author who wrote the singular
// loses their dependency links, and `autoSchedule` silently switches off
// with them.
const { deps, autoSchedule } = await rendered(
{ dependencyField: 'legacy_preds' },
'alias_singular',
);
expect(deps).toBe('L1+L2');
expect(autoSchedule).toBe('true');
});

it('the CANONICAL plural is read', async () => {
// The counter-probe: the assertion above must be about the singular limb,
// not about dependencies working at all.
const { deps, autoSchedule } = await rendered(
{ dependenciesField: 'canonical_preds' },
'alias_plural',
);
expect(deps).toBe('C1+C2');
expect(autoSchedule).toBe('true');
});

it('aimed at ONE field, the two spellings produce an identical result', async () => {
// The card's acceptance criterion, stated directly: the deprecation is a
// marker, so the singular and the plural are interchangeable at the read
// site. Compared value-to-value rather than each against a literal, so this
// stays true however `normalizeDependencies` evolves.
const viaLegacy = await rendered({ dependencyField: 'shared_preds' }, 'alias_same_a');
const viaCanonical = await rendered({ dependenciesField: 'shared_preds' }, 'alias_same_b');
expect(viaLegacy).toEqual(viaCanonical);
expect(viaLegacy.deps).toBe('S1+S2');
});

it('the canonical spelling WINS when a node carries both', async () => {
// Precedence, and the reason the deprecation marker is truthful: an author
// migrating key-by-key gets the canonical value, not a merge and not the
// legacy one.
const { deps } = await rendered(
{ dependenciesField: 'canonical_preds', dependencyField: 'legacy_preds' },
'alias_both',
);
expect(deps).toBe('C1+C2');
});

it('an EMPTY canonical value falls through to the alias — a property of `||`', async () => {
// Recorded, not designed. `dependenciesField: ''` is falsy, so the `||`
// reaches the singular; `??` would not. Pinned so that a future rewrite of
// that expression is a decision rather than an accident — and so the
// enforce-or-remove card inherits the real accept semantics instead of
// re-deriving them.
const { deps } = await rendered(
{ dependenciesField: '', dependencyField: 'legacy_preds' },
'alias_empty_canonical',
);
expect(deps).toBe('L1+L2');
});

it('neither spelling authored: no dependencies, and auto-scheduling stays OFF', async () => {
// Non-vacuity for every case above — without this, a harness that always
// reported the same string would pass them all.
const { deps, autoSchedule } = await rendered({}, 'alias_none');
expect(deps).toBe('');
expect(autoSchedule).toBe('false');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .changeset/6470-gantt-dependency-field-deprecated.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
---
'@object-ui/types': patch
'@object-ui/plugin-gantt': patch
---

`ObjectGanttSchema.dependencyField` is now marked `@deprecated` on both published
declaration faces, naming `dependenciesField` as the canonical spelling
(objectui#6470). Nothing is removed and nothing is rejected that was accepted
before.

**What the two spellings were.** `ObjectGanttSchema` declares both, and
`getGanttConfig`'s flat branch reads them with a `||`:
`dependenciesField: schema.dependenciesField || schema.dependencyField`.
`dependenciesField` is the spec's key (`@objectstack/spec`
`GanttConfigSchema.dependenciesField`); the singular `dependencyField` has NO
spec counterpart — zero occurrences across `packages/spec/src`, measured against
a live positive control on the plural. Until objectui#6051 declared the plural,
the singular was the ONLY dependencies spelling this interface carried, so for
the whole time the alias existed the published type taught the non-spec key and
hid the canonical one.

**What was missing was the ranking, not the behaviour.** The two were declared as
equals: nothing on either face said which one to author, so a reader — including
an AI writing metadata, which is the reader this project optimises for — had a
coin flip between a spec key and pre-spec vocabulary. The marker turns that coin
flip into a fact the type itself carries, and the zod mirror's description makes
it readable at runtime as well as in an editor.

This adopts the idiom already ruled for this exact shape rather than inventing a
second one: `KanbanConfig`'s pre-#2231 aliases (`groupField`, `cardFields`) carry
`/** @deprecated legacy alias for the spec's X */` plus
`.describe('Deprecated alias for X')`, and `dependencyField` now reads the same
way.

**⛔ Not a removal, deliberately.** Deleting the alias — or narrowing the
renderer's `||` — would break every author who wrote the singular and narrow the
accept set of a published surface. That is a maintainer decision on a future
enforce-or-remove card once the deprecation has sat a release, and it is
explicitly excluded here. Two pins hold the line in both directions:
`packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts`
fails if the marker goes missing AND if the alias stops being declared or
accepted, and `packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx`
fails if the `||` limb is dropped — the two spellings must keep resolving to the
same config, with the canonical one winning when both carry a value.

`packages/plugin-gantt/README.md`'s `ObjectGanttSchema` example authored the
singular; it was the only in-repo site that did, and it now authors the plural
with the alias named as legacy. No runtime code, fixture, example app or catalog
schema authored it.
7 changes: 6 additions & 1 deletion packages/plugin-gantt/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -670,10 +670,15 @@ const gantt: ObjectGanttSchema = {
startDateField: 'start_date',
endDateField: 'end_date',
progressField: 'completion_percentage',
dependencyField: 'dependent_task_ids',
dependenciesField: 'dependent_task_ids',
};
```

`dependenciesField` (plural) is the spec's spelling and the one to author. The
singular `dependencyField` is a `@deprecated` legacy alias: `ObjectGantt` still
reads it (`dependenciesField || dependencyField`), so existing metadata keeps
working, but new metadata should not use it.

For a list view served under the `gantt` view type, the same configuration is a
`gantt` block on `ListViewSchema` (typed by `GanttConfig`, also from
`@object-ui/types`) rather than top-level keys — `ObjectGantt` reads either
Expand Down
176 changes: 176 additions & 0 deletions packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* Behaviour pin — deprecating `dependencyField` changed NOTHING at the read site
* (objectui#6470).
*
* ## What this file is for
*
* objectui#6470 marks the singular `dependencyField` `@deprecated` on both
* published declaration faces (`packages/types/src/objectql.ts` and its zod
* mirror), naming the spec's `dependenciesField` as canonical. The marker is
* documentation; `getGanttConfig`'s flat branch still reads
* `dependenciesField || dependencyField` and every author who wrote the singular
* must keep rendering exactly as before.
*
* ⛔ Removal was explicitly excluded from that card and deferred to a future
* enforce-or-remove decision. This suite is what makes the exclusion mechanical:
* drop the `|| schema.dependencyField` limb and the first two cases below go red
* naming the break, rather than a published surface silently losing an accepted
* spelling.
*
* ## Why the two spellings name DIFFERENT record fields here
*
* A fixture where both spellings point at the same field can only prove that
* SOMETHING resolved; it cannot show WHICH limb of the `||` produced it, so it
* would stay green with either limb deleted. Pointing them at different fields
* (`legacy_preds` vs `canonical_preds`, with values that cannot be confused)
* makes the resolved config visible in the rendered tasks — the same technique
* `ObjectGantt.blockPrecedence.test.tsx` uses for the block/flat flip.
*
* The "same config" half the card asks for is then pinned directly, by the
* third case: the two spellings aimed at ONE field produce byte-identical task
* dependencies.
*
* ## The observable
*
* `dependenciesField` is not passed to `GanttView` as a config value — it is
* consumed on the way in (`dependencies: normalizeDependencies(record[dep])`)
* and again as the `autoSchedule` / `onDependencyCreate` switches. Both are
* observed: the per-task dependency ids say WHICH field was read, and
* `autoSchedule` says the config resolved to a truthy field name at all.
*/
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { ObjectGantt } from './ObjectGantt';

vi.mock('./GanttView', () => ({
GanttView: ({ tasks, autoSchedule }: any) => (
<div
data-testid="gantt-view"
data-count={tasks.length}
data-deps={tasks.map((t: any) => t.dependencies.join('+')).join('|')}
data-autoschedule={String(!!autoSchedule)}
/>
),
}));

vi.mock('./ResourceWorkload', () => ({
ResourceWorkload: ({ tasks }: any) => <div data-testid="resource-workload" data-count={tasks.length} />,
}));

/**
* One record carrying BOTH candidate fields, with values that cannot be
* confused: the legacy field's ids start `L`, the canonical field's start `C`.
*/
const INLINE = [
{
id: '1',
name: 'Alpha',
start: '2024-01-01',
end: '2024-01-05',
legacy_preds: 'L1,L2',
canonical_preds: 'C1,C2',
shared_preds: 'S1,S2',
},
];

const DATES = { startDateField: 'start', endDateField: 'end', titleField: 'name' };

async function rendered(extra: Record<string, unknown>, objectName: string) {
const { container } = render(
<ObjectGantt
schema={
{
type: 'object-gantt',
objectName,
...DATES,
data: { provider: 'value', items: INLINE },
...extra,
} as any
}
/>,
);
const el = () => container.querySelector('[data-testid="gantt-view"]') as HTMLElement;
await waitFor(() => expect(el()?.getAttribute('data-count')).toBe('1'));
return {
deps: el().getAttribute('data-deps'),
autoSchedule: el().getAttribute('data-autoschedule'),
};
}

describe('both dependency spellings still resolve to the same config (objectui#6470)', () => {
it('the DEPRECATED singular is still read', async () => {
// ⛔ The removal guard. Deleting `|| schema.dependencyField` from
// `getGanttConfig` turns this red — every author who wrote the singular
// loses their dependency links, and `autoSchedule` silently switches off
// with them.
const { deps, autoSchedule } = await rendered(
{ dependencyField: 'legacy_preds' },
'alias_singular',
);
expect(deps).toBe('L1+L2');
expect(autoSchedule).toBe('true');
});

it('the CANONICAL plural is read', async () => {
// The counter-probe: the assertion above must be about the singular limb,
// not about dependencies working at all.
const { deps, autoSchedule } = await rendered(
{ dependenciesField: 'canonical_preds' },
'alias_plural',
);
expect(deps).toBe('C1+C2');
expect(autoSchedule).toBe('true');
});

it('aimed at ONE field, the two spellings produce an identical result', async () => {
// The card's acceptance criterion, stated directly: the deprecation is a
// marker, so the singular and the plural are interchangeable at the read
// site. Compared value-to-value rather than each against a literal, so this
// stays true however `normalizeDependencies` evolves.
const viaLegacy = await rendered({ dependencyField: 'shared_preds' }, 'alias_same_a');
const viaCanonical = await rendered({ dependenciesField: 'shared_preds' }, 'alias_same_b');
expect(viaLegacy).toEqual(viaCanonical);
expect(viaLegacy.deps).toBe('S1+S2');
});

it('the canonical spelling WINS when a node carries both', async () => {
// Precedence, and the reason the deprecation marker is truthful: an author
// migrating key-by-key gets the canonical value, not a merge and not the
// legacy one.
const { deps } = await rendered(
{ dependenciesField: 'canonical_preds', dependencyField: 'legacy_preds' },
'alias_both',
);
expect(deps).toBe('C1+C2');
});

it('an EMPTY canonical value falls through to the alias — a property of `||`', async () => {
// Recorded, not designed. `dependenciesField: ''` is falsy, so the `||`
// reaches the singular; `??` would not. Pinned so that a future rewrite of
// that expression is a decision rather than an accident — and so the
// enforce-or-remove card inherits the real accept semantics instead of
// re-deriving them.
const { deps } = await rendered(
{ dependenciesField: '', dependencyField: 'legacy_preds' },
'alias_empty_canonical',
);
expect(deps).toBe('L1+L2');
});

it('neither spelling authored: no dependencies, and auto-scheduling stays OFF', async () => {
// Non-vacuity for every case above — without this, a harness that always
// reported the same string would pass them all.
const { deps, autoSchedule } = await rendered({}, 'alias_none');
expect(deps).toBe('');
expect(autoSchedule).toBe('false');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .changeset/6470-gantt-dependency-field-deprecated.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
---
'@object-ui/types': patch
'@object-ui/plugin-gantt': patch
---

`ObjectGanttSchema.dependencyField` is now marked `@deprecated` on both published
declaration faces, naming `dependenciesField` as the canonical spelling
(objectui#6470). Nothing is removed and nothing is rejected that was accepted
before.

**What the two spellings were.** `ObjectGanttSchema` declares both, and
`getGanttConfig`'s flat branch reads them with a `||`:
`dependenciesField: schema.dependenciesField || schema.dependencyField`.
`dependenciesField` is the spec's key (`@objectstack/spec`
`GanttConfigSchema.dependenciesField`); the singular `dependencyField` has NO
spec counterpart — zero occurrences across `packages/spec/src`, measured against
a live positive control on the plural. Until objectui#6051 declared the plural,
the singular was the ONLY dependencies spelling this interface carried, so for
the whole time the alias existed the published type taught the non-spec key and
hid the canonical one.

**What was missing was the ranking, not the behaviour.** The two were declared as
equals: nothing on either face said which one to author, so a reader — including
an AI writing metadata, which is the reader this project optimises for — had a
coin flip between a spec key and pre-spec vocabulary. The marker turns that coin
flip into a fact the type itself carries, and the zod mirror's description makes
it readable at runtime as well as in an editor.

This adopts the idiom already ruled for this exact shape rather than inventing a
second one: `KanbanConfig`'s pre-#2231 aliases (`groupField`, `cardFields`) carry
`/** @deprecated legacy alias for the spec's X */` plus
`.describe('Deprecated alias for X')`, and `dependencyField` now reads the same
way.

**⛔ Not a removal, deliberately.** Deleting the alias — or narrowing the
renderer's `||` — would break every author who wrote the singular and narrow the
accept set of a published surface. That is a maintainer decision on a future
enforce-or-remove card once the deprecation has sat a release, and it is
explicitly excluded here. Two pins hold the line in both directions:
`packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts`
fails if the marker goes missing AND if the alias stops being declared or
accepted, and `packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx`
fails if the `||` limb is dropped — the two spellings must keep resolving to the
same config, with the canonical one winning when both carry a value.

`packages/plugin-gantt/README.md`'s `ObjectGanttSchema` example authored the
singular; it was the only in-repo site that did, and it now authors the plural
with the alias named as legacy. No runtime code, fixture, example app or catalog
schema authored it.
7 changes: 6 additions & 1 deletion packages/plugin-gantt/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -670,10 +670,15 @@ const gantt: ObjectGanttSchema = {
startDateField: 'start_date',
endDateField: 'end_date',
progressField: 'completion_percentage',
dependencyField: 'dependent_task_ids',
dependenciesField: 'dependent_task_ids',
};
```

`dependenciesField` (plural) is the spec's spelling and the one to author. The
singular `dependencyField` is a `@deprecated` legacy alias: `ObjectGantt` still
reads it (`dependenciesField || dependencyField`), so existing metadata keeps
working, but new metadata should not use it.

For a list view served under the `gantt` view type, the same configuration is a
`gantt` block on `ListViewSchema` (typed by `GanttConfig`, also from
`@object-ui/types`) rather than top-level keys — `ObjectGantt` reads either
Expand Down
176 changes: 176 additions & 0 deletions packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* Behaviour pin — deprecating `dependencyField` changed NOTHING at the read site
* (objectui#6470).
*
* ## What this file is for
*
* objectui#6470 marks the singular `dependencyField` `@deprecated` on both
* published declaration faces (`packages/types/src/objectql.ts` and its zod
* mirror), naming the spec's `dependenciesField` as canonical. The marker is
* documentation; `getGanttConfig`'s flat branch still reads
* `dependenciesField || dependencyField` and every author who wrote the singular
* must keep rendering exactly as before.
*
* ⛔ Removal was explicitly excluded from that card and deferred to a future
* enforce-or-remove decision. This suite is what makes the exclusion mechanical:
* drop the `|| schema.dependencyField` limb and the first two cases below go red
* naming the break, rather than a published surface silently losing an accepted
* spelling.
*
* ## Why the two spellings name DIFFERENT record fields here
*
* A fixture where both spellings point at the same field can only prove that
* SOMETHING resolved; it cannot show WHICH limb of the `||` produced it, so it
* would stay green with either limb deleted. Pointing them at different fields
* (`legacy_preds` vs `canonical_preds`, with values that cannot be confused)
* makes the resolved config visible in the rendered tasks — the same technique
* `ObjectGantt.blockPrecedence.test.tsx` uses for the block/flat flip.
*
* The "same config" half the card asks for is then pinned directly, by the
* third case: the two spellings aimed at ONE field produce byte-identical task
* dependencies.
*
* ## The observable
*
* `dependenciesField` is not passed to `GanttView` as a config value — it is
* consumed on the way in (`dependencies: normalizeDependencies(record[dep])`)
* and again as the `autoSchedule` / `onDependencyCreate` switches. Both are
* observed: the per-task dependency ids say WHICH field was read, and
* `autoSchedule` says the config resolved to a truthy field name at all.
*/
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { ObjectGantt } from './ObjectGantt';

vi.mock('./GanttView', () => ({
GanttView: ({ tasks, autoSchedule }: any) => (
<div
data-testid="gantt-view"
data-count={tasks.length}
data-deps={tasks.map((t: any) => t.dependencies.join('+')).join('|')}
data-autoschedule={String(!!autoSchedule)}
/>
),
}));

vi.mock('./ResourceWorkload', () => ({
ResourceWorkload: ({ tasks }: any) => <div data-testid="resource-workload" data-count={tasks.length} />,
}));

/**
* One record carrying BOTH candidate fields, with values that cannot be
* confused: the legacy field's ids start `L`, the canonical field's start `C`.
*/
const INLINE = [
{
id: '1',
name: 'Alpha',
start: '2024-01-01',
end: '2024-01-05',
legacy_preds: 'L1,L2',
canonical_preds: 'C1,C2',
shared_preds: 'S1,S2',
},
];

const DATES = { startDateField: 'start', endDateField: 'end', titleField: 'name' };

async function rendered(extra: Record<string, unknown>, objectName: string) {
const { container } = render(
<ObjectGantt
schema={
{
type: 'object-gantt',
objectName,
...DATES,
data: { provider: 'value', items: INLINE },
...extra,
} as any
}
/>,
);
const el = () => container.querySelector('[data-testid="gantt-view"]') as HTMLElement;
await waitFor(() => expect(el()?.getAttribute('data-count')).toBe('1'));
return {
deps: el().getAttribute('data-deps'),
autoSchedule: el().getAttribute('data-autoschedule'),
};
}

describe('both dependency spellings still resolve to the same config (objectui#6470)', () => {
it('the DEPRECATED singular is still read', async () => {
// ⛔ The removal guard. Deleting `|| schema.dependencyField` from
// `getGanttConfig` turns this red — every author who wrote the singular
// loses their dependency links, and `autoSchedule` silently switches off
// with them.
const { deps, autoSchedule } = await rendered(
{ dependencyField: 'legacy_preds' },
'alias_singular',
);
expect(deps).toBe('L1+L2');
expect(autoSchedule).toBe('true');
});

it('the CANONICAL plural is read', async () => {
// The counter-probe: the assertion above must be about the singular limb,
// not about dependencies working at all.
const { deps, autoSchedule } = await rendered(
{ dependenciesField: 'canonical_preds' },
'alias_plural',
);
expect(deps).toBe('C1+C2');
expect(autoSchedule).toBe('true');
});

it('aimed at ONE field, the two spellings produce an identical result', async () => {
// The card's acceptance criterion, stated directly: the deprecation is a
// marker, so the singular and the plural are interchangeable at the read
// site. Compared value-to-value rather than each against a literal, so this
// stays true however `normalizeDependencies` evolves.
const viaLegacy = await rendered({ dependencyField: 'shared_preds' }, 'alias_same_a');
const viaCanonical = await rendered({ dependenciesField: 'shared_preds' }, 'alias_same_b');
expect(viaLegacy).toEqual(viaCanonical);
expect(viaLegacy.deps).toBe('S1+S2');
});

it('the canonical spelling WINS when a node carries both', async () => {
// Precedence, and the reason the deprecation marker is truthful: an author
// migrating key-by-key gets the canonical value, not a merge and not the
// legacy one.
const { deps } = await rendered(
{ dependenciesField: 'canonical_preds', dependencyField: 'legacy_preds' },
'alias_both',
);
expect(deps).toBe('C1+C2');
});

it('an EMPTY canonical value falls through to the alias — a property of `||`', async () => {
// Recorded, not designed. `dependenciesField: ''` is falsy, so the `||`
// reaches the singular; `??` would not. Pinned so that a future rewrite of
// that expression is a decision rather than an accident — and so the
// enforce-or-remove card inherits the real accept semantics instead of
// re-deriving them.
const { deps } = await rendered(
{ dependenciesField: '', dependencyField: 'legacy_preds' },
'alias_empty_canonical',
);
expect(deps).toBe('L1+L2');
});

it('neither spelling authored: no dependencies, and auto-scheduling stays OFF', async () => {
// Non-vacuity for every case above — without this, a harness that always
// reported the same string would pass them all.
const { deps, autoSchedule } = await rendered({}, 'alias_none');
expect(deps).toBe('');
expect(autoSchedule).toBe('false');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .changeset/6470-gantt-dependency-field-deprecated.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
---
'@object-ui/types': patch
'@object-ui/plugin-gantt': patch
---

`ObjectGanttSchema.dependencyField` is now marked `@deprecated` on both published
declaration faces, naming `dependenciesField` as the canonical spelling
(objectui#6470). Nothing is removed and nothing is rejected that was accepted
before.

**What the two spellings were.** `ObjectGanttSchema` declares both, and
`getGanttConfig`'s flat branch reads them with a `||`:
`dependenciesField: schema.dependenciesField || schema.dependencyField`.
`dependenciesField` is the spec's key (`@objectstack/spec`
`GanttConfigSchema.dependenciesField`); the singular `dependencyField` has NO
spec counterpart — zero occurrences across `packages/spec/src`, measured against
a live positive control on the plural. Until objectui#6051 declared the plural,
the singular was the ONLY dependencies spelling this interface carried, so for
the whole time the alias existed the published type taught the non-spec key and
hid the canonical one.

**What was missing was the ranking, not the behaviour.** The two were declared as
equals: nothing on either face said which one to author, so a reader — including
an AI writing metadata, which is the reader this project optimises for — had a
coin flip between a spec key and pre-spec vocabulary. The marker turns that coin
flip into a fact the type itself carries, and the zod mirror's description makes
it readable at runtime as well as in an editor.

This adopts the idiom already ruled for this exact shape rather than inventing a
second one: `KanbanConfig`'s pre-#2231 aliases (`groupField`, `cardFields`) carry
`/** @deprecated legacy alias for the spec's X */` plus
`.describe('Deprecated alias for X')`, and `dependencyField` now reads the same
way.

**⛔ Not a removal, deliberately.** Deleting the alias — or narrowing the
renderer's `||` — would break every author who wrote the singular and narrow the
accept set of a published surface. That is a maintainer decision on a future
enforce-or-remove card once the deprecation has sat a release, and it is
explicitly excluded here. Two pins hold the line in both directions:
`packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts`
fails if the marker goes missing AND if the alias stops being declared or
accepted, and `packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx`
fails if the `||` limb is dropped — the two spellings must keep resolving to the
same config, with the canonical one winning when both carry a value.

`packages/plugin-gantt/README.md`'s `ObjectGanttSchema` example authored the
singular; it was the only in-repo site that did, and it now authors the plural
with the alias named as legacy. No runtime code, fixture, example app or catalog
schema authored it.
7 changes: 6 additions & 1 deletion packages/plugin-gantt/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -670,10 +670,15 @@ const gantt: ObjectGanttSchema = {
startDateField: 'start_date',
endDateField: 'end_date',
progressField: 'completion_percentage',
dependencyField: 'dependent_task_ids',
dependenciesField: 'dependent_task_ids',
};
```

`dependenciesField` (plural) is the spec's spelling and the one to author. The
singular `dependencyField` is a `@deprecated` legacy alias: `ObjectGantt` still
reads it (`dependenciesField || dependencyField`), so existing metadata keeps
working, but new metadata should not use it.

For a list view served under the `gantt` view type, the same configuration is a
`gantt` block on `ListViewSchema` (typed by `GanttConfig`, also from
`@object-ui/types`) rather than top-level keys — `ObjectGantt` reads either
Expand Down
176 changes: 176 additions & 0 deletions packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* Behaviour pin — deprecating `dependencyField` changed NOTHING at the read site
* (objectui#6470).
*
* ## What this file is for
*
* objectui#6470 marks the singular `dependencyField` `@deprecated` on both
* published declaration faces (`packages/types/src/objectql.ts` and its zod
* mirror), naming the spec's `dependenciesField` as canonical. The marker is
* documentation; `getGanttConfig`'s flat branch still reads
* `dependenciesField || dependencyField` and every author who wrote the singular
* must keep rendering exactly as before.
*
* ⛔ Removal was explicitly excluded from that card and deferred to a future
* enforce-or-remove decision. This suite is what makes the exclusion mechanical:
* drop the `|| schema.dependencyField` limb and the first two cases below go red
* naming the break, rather than a published surface silently losing an accepted
* spelling.
*
* ## Why the two spellings name DIFFERENT record fields here
*
* A fixture where both spellings point at the same field can only prove that
* SOMETHING resolved; it cannot show WHICH limb of the `||` produced it, so it
* would stay green with either limb deleted. Pointing them at different fields
* (`legacy_preds` vs `canonical_preds`, with values that cannot be confused)
* makes the resolved config visible in the rendered tasks — the same technique
* `ObjectGantt.blockPrecedence.test.tsx` uses for the block/flat flip.
*
* The "same config" half the card asks for is then pinned directly, by the
* third case: the two spellings aimed at ONE field produce byte-identical task
* dependencies.
*
* ## The observable
*
* `dependenciesField` is not passed to `GanttView` as a config value — it is
* consumed on the way in (`dependencies: normalizeDependencies(record[dep])`)
* and again as the `autoSchedule` / `onDependencyCreate` switches. Both are
* observed: the per-task dependency ids say WHICH field was read, and
* `autoSchedule` says the config resolved to a truthy field name at all.
*/
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { ObjectGantt } from './ObjectGantt';

vi.mock('./GanttView', () => ({
GanttView: ({ tasks, autoSchedule }: any) => (
<div
data-testid="gantt-view"
data-count={tasks.length}
data-deps={tasks.map((t: any) => t.dependencies.join('+')).join('|')}
data-autoschedule={String(!!autoSchedule)}
/>
),
}));

vi.mock('./ResourceWorkload', () => ({
ResourceWorkload: ({ tasks }: any) => <div data-testid="resource-workload" data-count={tasks.length} />,
}));

/**
* One record carrying BOTH candidate fields, with values that cannot be
* confused: the legacy field's ids start `L`, the canonical field's start `C`.
*/
const INLINE = [
{
id: '1',
name: 'Alpha',
start: '2024-01-01',
end: '2024-01-05',
legacy_preds: 'L1,L2',
canonical_preds: 'C1,C2',
shared_preds: 'S1,S2',
},
];

const DATES = { startDateField: 'start', endDateField: 'end', titleField: 'name' };

async function rendered(extra: Record<string, unknown>, objectName: string) {
const { container } = render(
<ObjectGantt
schema={
{
type: 'object-gantt',
objectName,
...DATES,
data: { provider: 'value', items: INLINE },
...extra,
} as any
}
/>,
);
const el = () => container.querySelector('[data-testid="gantt-view"]') as HTMLElement;
await waitFor(() => expect(el()?.getAttribute('data-count')).toBe('1'));
return {
deps: el().getAttribute('data-deps'),
autoSchedule: el().getAttribute('data-autoschedule'),
};
}

describe('both dependency spellings still resolve to the same config (objectui#6470)', () => {
it('the DEPRECATED singular is still read', async () => {
// ⛔ The removal guard. Deleting `|| schema.dependencyField` from
// `getGanttConfig` turns this red — every author who wrote the singular
// loses their dependency links, and `autoSchedule` silently switches off
// with them.
const { deps, autoSchedule } = await rendered(
{ dependencyField: 'legacy_preds' },
'alias_singular',
);
expect(deps).toBe('L1+L2');
expect(autoSchedule).toBe('true');
});

it('the CANONICAL plural is read', async () => {
// The counter-probe: the assertion above must be about the singular limb,
// not about dependencies working at all.
const { deps, autoSchedule } = await rendered(
{ dependenciesField: 'canonical_preds' },
'alias_plural',
);
expect(deps).toBe('C1+C2');
expect(autoSchedule).toBe('true');
});

it('aimed at ONE field, the two spellings produce an identical result', async () => {
// The card's acceptance criterion, stated directly: the deprecation is a
// marker, so the singular and the plural are interchangeable at the read
// site. Compared value-to-value rather than each against a literal, so this
// stays true however `normalizeDependencies` evolves.
const viaLegacy = await rendered({ dependencyField: 'shared_preds' }, 'alias_same_a');
const viaCanonical = await rendered({ dependenciesField: 'shared_preds' }, 'alias_same_b');
expect(viaLegacy).toEqual(viaCanonical);
expect(viaLegacy.deps).toBe('S1+S2');
});

it('the canonical spelling WINS when a node carries both', async () => {
// Precedence, and the reason the deprecation marker is truthful: an author
// migrating key-by-key gets the canonical value, not a merge and not the
// legacy one.
const { deps } = await rendered(
{ dependenciesField: 'canonical_preds', dependencyField: 'legacy_preds' },
'alias_both',
);
expect(deps).toBe('C1+C2');
});

it('an EMPTY canonical value falls through to the alias — a property of `||`', async () => {
// Recorded, not designed. `dependenciesField: ''` is falsy, so the `||`
// reaches the singular; `??` would not. Pinned so that a future rewrite of
// that expression is a decision rather than an accident — and so the
// enforce-or-remove card inherits the real accept semantics instead of
// re-deriving them.
const { deps } = await rendered(
{ dependenciesField: '', dependencyField: 'legacy_preds' },
'alias_empty_canonical',
);
expect(deps).toBe('L1+L2');
});

it('neither spelling authored: no dependencies, and auto-scheduling stays OFF', async () => {
// Non-vacuity for every case above — without this, a harness that always
// reported the same string would pass them all.
const { deps, autoSchedule } = await rendered({}, 'alias_none');
expect(deps).toBe('');
expect(autoSchedule).toBe('false');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .changeset/6470-gantt-dependency-field-deprecated.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
---
'@object-ui/types': patch
'@object-ui/plugin-gantt': patch
---

`ObjectGanttSchema.dependencyField` is now marked `@deprecated` on both published
declaration faces, naming `dependenciesField` as the canonical spelling
(objectui#6470). Nothing is removed and nothing is rejected that was accepted
before.

**What the two spellings were.** `ObjectGanttSchema` declares both, and
`getGanttConfig`'s flat branch reads them with a `||`:
`dependenciesField: schema.dependenciesField || schema.dependencyField`.
`dependenciesField` is the spec's key (`@objectstack/spec`
`GanttConfigSchema.dependenciesField`); the singular `dependencyField` has NO
spec counterpart — zero occurrences across `packages/spec/src`, measured against
a live positive control on the plural. Until objectui#6051 declared the plural,
the singular was the ONLY dependencies spelling this interface carried, so for
the whole time the alias existed the published type taught the non-spec key and
hid the canonical one.

**What was missing was the ranking, not the behaviour.** The two were declared as
equals: nothing on either face said which one to author, so a reader — including
an AI writing metadata, which is the reader this project optimises for — had a
coin flip between a spec key and pre-spec vocabulary. The marker turns that coin
flip into a fact the type itself carries, and the zod mirror's description makes
it readable at runtime as well as in an editor.

This adopts the idiom already ruled for this exact shape rather than inventing a
second one: `KanbanConfig`'s pre-#2231 aliases (`groupField`, `cardFields`) carry
`/** @deprecated legacy alias for the spec's X */` plus
`.describe('Deprecated alias for X')`, and `dependencyField` now reads the same
way.

**⛔ Not a removal, deliberately.** Deleting the alias — or narrowing the
renderer's `||` — would break every author who wrote the singular and narrow the
accept set of a published surface. That is a maintainer decision on a future
enforce-or-remove card once the deprecation has sat a release, and it is
explicitly excluded here. Two pins hold the line in both directions:
`packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts`
fails if the marker goes missing AND if the alias stops being declared or
accepted, and `packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx`
fails if the `||` limb is dropped — the two spellings must keep resolving to the
same config, with the canonical one winning when both carry a value.

`packages/plugin-gantt/README.md`'s `ObjectGanttSchema` example authored the
singular; it was the only in-repo site that did, and it now authors the plural
with the alias named as legacy. No runtime code, fixture, example app or catalog
schema authored it.
7 changes: 6 additions & 1 deletion packages/plugin-gantt/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -670,10 +670,15 @@ const gantt: ObjectGanttSchema = {
startDateField: 'start_date',
endDateField: 'end_date',
progressField: 'completion_percentage',
dependencyField: 'dependent_task_ids',
dependenciesField: 'dependent_task_ids',
};
```

`dependenciesField` (plural) is the spec's spelling and the one to author. The
singular `dependencyField` is a `@deprecated` legacy alias: `ObjectGantt` still
reads it (`dependenciesField || dependencyField`), so existing metadata keeps
working, but new metadata should not use it.

For a list view served under the `gantt` view type, the same configuration is a
`gantt` block on `ListViewSchema` (typed by `GanttConfig`, also from
`@object-ui/types`) rather than top-level keys — `ObjectGantt` reads either
Expand Down
176 changes: 176 additions & 0 deletions packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* Behaviour pin — deprecating `dependencyField` changed NOTHING at the read site
* (objectui#6470).
*
* ## What this file is for
*
* objectui#6470 marks the singular `dependencyField` `@deprecated` on both
* published declaration faces (`packages/types/src/objectql.ts` and its zod
* mirror), naming the spec's `dependenciesField` as canonical. The marker is
* documentation; `getGanttConfig`'s flat branch still reads
* `dependenciesField || dependencyField` and every author who wrote the singular
* must keep rendering exactly as before.
*
* ⛔ Removal was explicitly excluded from that card and deferred to a future
* enforce-or-remove decision. This suite is what makes the exclusion mechanical:
* drop the `|| schema.dependencyField` limb and the first two cases below go red
* naming the break, rather than a published surface silently losing an accepted
* spelling.
*
* ## Why the two spellings name DIFFERENT record fields here
*
* A fixture where both spellings point at the same field can only prove that
* SOMETHING resolved; it cannot show WHICH limb of the `||` produced it, so it
* would stay green with either limb deleted. Pointing them at different fields
* (`legacy_preds` vs `canonical_preds`, with values that cannot be confused)
* makes the resolved config visible in the rendered tasks — the same technique
* `ObjectGantt.blockPrecedence.test.tsx` uses for the block/flat flip.
*
* The "same config" half the card asks for is then pinned directly, by the
* third case: the two spellings aimed at ONE field produce byte-identical task
* dependencies.
*
* ## The observable
*
* `dependenciesField` is not passed to `GanttView` as a config value — it is
* consumed on the way in (`dependencies: normalizeDependencies(record[dep])`)
* and again as the `autoSchedule` / `onDependencyCreate` switches. Both are
* observed: the per-task dependency ids say WHICH field was read, and
* `autoSchedule` says the config resolved to a truthy field name at all.
*/
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { ObjectGantt } from './ObjectGantt';

vi.mock('./GanttView', () => ({
GanttView: ({ tasks, autoSchedule }: any) => (
<div
data-testid="gantt-view"
data-count={tasks.length}
data-deps={tasks.map((t: any) => t.dependencies.join('+')).join('|')}
data-autoschedule={String(!!autoSchedule)}
/>
),
}));

vi.mock('./ResourceWorkload', () => ({
ResourceWorkload: ({ tasks }: any) => <div data-testid="resource-workload" data-count={tasks.length} />,
}));

/**
* One record carrying BOTH candidate fields, with values that cannot be
* confused: the legacy field's ids start `L`, the canonical field's start `C`.
*/
const INLINE = [
{
id: '1',
name: 'Alpha',
start: '2024-01-01',
end: '2024-01-05',
legacy_preds: 'L1,L2',
canonical_preds: 'C1,C2',
shared_preds: 'S1,S2',
},
];

const DATES = { startDateField: 'start', endDateField: 'end', titleField: 'name' };

async function rendered(extra: Record<string, unknown>, objectName: string) {
const { container } = render(
<ObjectGantt
schema={
{
type: 'object-gantt',
objectName,
...DATES,
data: { provider: 'value', items: INLINE },
...extra,
} as any
}
/>,
);
const el = () => container.querySelector('[data-testid="gantt-view"]') as HTMLElement;
await waitFor(() => expect(el()?.getAttribute('data-count')).toBe('1'));
return {
deps: el().getAttribute('data-deps'),
autoSchedule: el().getAttribute('data-autoschedule'),
};
}

describe('both dependency spellings still resolve to the same config (objectui#6470)', () => {
it('the DEPRECATED singular is still read', async () => {
// ⛔ The removal guard. Deleting `|| schema.dependencyField` from
// `getGanttConfig` turns this red — every author who wrote the singular
// loses their dependency links, and `autoSchedule` silently switches off
// with them.
const { deps, autoSchedule } = await rendered(
{ dependencyField: 'legacy_preds' },
'alias_singular',
);
expect(deps).toBe('L1+L2');
expect(autoSchedule).toBe('true');
});

it('the CANONICAL plural is read', async () => {
// The counter-probe: the assertion above must be about the singular limb,
// not about dependencies working at all.
const { deps, autoSchedule } = await rendered(
{ dependenciesField: 'canonical_preds' },
'alias_plural',
);
expect(deps).toBe('C1+C2');
expect(autoSchedule).toBe('true');
});

it('aimed at ONE field, the two spellings produce an identical result', async () => {
// The card's acceptance criterion, stated directly: the deprecation is a
// marker, so the singular and the plural are interchangeable at the read
// site. Compared value-to-value rather than each against a literal, so this
// stays true however `normalizeDependencies` evolves.
const viaLegacy = await rendered({ dependencyField: 'shared_preds' }, 'alias_same_a');
const viaCanonical = await rendered({ dependenciesField: 'shared_preds' }, 'alias_same_b');
expect(viaLegacy).toEqual(viaCanonical);
expect(viaLegacy.deps).toBe('S1+S2');
});

it('the canonical spelling WINS when a node carries both', async () => {
// Precedence, and the reason the deprecation marker is truthful: an author
// migrating key-by-key gets the canonical value, not a merge and not the
// legacy one.
const { deps } = await rendered(
{ dependenciesField: 'canonical_preds', dependencyField: 'legacy_preds' },
'alias_both',
);
expect(deps).toBe('C1+C2');
});

it('an EMPTY canonical value falls through to the alias — a property of `||`', async () => {
// Recorded, not designed. `dependenciesField: ''` is falsy, so the `||`
// reaches the singular; `??` would not. Pinned so that a future rewrite of
// that expression is a decision rather than an accident — and so the
// enforce-or-remove card inherits the real accept semantics instead of
// re-deriving them.
const { deps } = await rendered(
{ dependenciesField: '', dependencyField: 'legacy_preds' },
'alias_empty_canonical',
);
expect(deps).toBe('L1+L2');
});

it('neither spelling authored: no dependencies, and auto-scheduling stays OFF', async () => {
// Non-vacuity for every case above — without this, a harness that always
// reported the same string would pass them all.
const { deps, autoSchedule } = await rendered({}, 'alias_none');
expect(deps).toBe('');
expect(autoSchedule).toBe('false');
});
});
Loading
Loading